diff --git a/.gitignore b/.gitignore index 0291f3a..9c9953e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ src/config.php src/ui/config.php lib/ui/fontawesome-free-6.1.2-web/ templates/README.md +templates/docs/ .env *.bak *.crt diff --git a/README.md b/README.md index 216741d..70f150d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# glftpd-webui (v4) +# glftpd-webui (V5) ## /gl wɛb ʊi/ @@ -103,6 +103,7 @@ Cutting-edge tech used: - Sitewho: [pywho](https://github.com/silv3rr/pywho), [ansi-escapes-to-html](https://github.com/neilime/ansi-escapes-to-html) - Markdown: [parsedown](https://github.com/erusev/parsedown) - Auth: [ip-lib](https://github.com/mlocati/ip-lib), _[apr1-md5](https://github.com/whitehat101/apr1-md5)_, _[PHP-Htpasswd](https://github.com/ozanhazer/PHP-Htpasswd)_ +- Graphs: [svg-chart-builder](https://github.com/xanpena/svg-chart-builder) # Screenshots diff --git a/assets/css/dark.css b/assets/css/dark.css index 4d992b3..2d0c1ab 100644 --- a/assets/css/dark.css +++ b/assets/css/dark.css @@ -15,6 +15,7 @@ background-color: lightgray; } [data-theme='dark'] button, +[data-theme='dark'] button a, [data-theme='dark'] button.btn { color: #ccc; background-color: #6c757d; @@ -47,6 +48,9 @@ border-color: whitesmoke; color: black; } +[data-theme='dark'] .modal-content { + background-color: lightgray; +} [data-theme='dark'] .modal-footer { background-color: lightgray; border-color: whitesmoke; @@ -104,6 +108,10 @@ [data-theme='dark'] .alert .btn-link { color: darkcyan; } +[data-theme='dark'] .stats { + color:#222; + background-color: #aaa; +} [data-theme='dark'] #spy_api_result { background-color: #6c757d; } diff --git a/assets/css/spy.css b/assets/css/spy.css index fb60299..dd34dc8 100644 --- a/assets/css/spy.css +++ b/assets/css/spy.css @@ -1,13 +1,7 @@ -/* SPY + OVERRIDES */ +@import url('style.css'); /* body { - /* - background-color: black; - background-color: whitesmoke; - color: white; - */ - /* font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; background-color: var(--bg-color); color: var(--font-color); @@ -292,34 +286,16 @@ a:link, a:visited, a:hover, a:active { } */ +/* dark theme + https://dev.to/ananyaneogi/create-a-dark-light-mode-switch-with-css-variables-34l8 */ :root { - /* - --text-color-normal: #0a244d; - --text-color-light: #8cabd9; - - --primary-color: #302AE6; - --secondary-color: #536390; - */ --font-color: #424242; - /* --bg-color: #fff; */ --bg-color: whitesmoke; - /* --heading-color: #292922; */ } [data-theme='dark'] { - /* - --text-color-normal: hsl(210, 10%, 62%); - --text-color-light: hsl(210, 15%, 35%); - --text-color-richer: hsl(210, 50%, 72%); - --text-color-highlight: hsl(25, 70%, 45%); - - --primary-color: #9A97F3; - --secondary-color: #818cab; - */ - /* --font-color: #e1e1ff; */ --font-color: #929292; --bg-color: #161625; - /* --heading-color: #818cab; */ } [data-theme='dark'] .settings thead { color: lightgray @@ -372,7 +348,6 @@ a:link, a:visited, a:hover, a:active { [data-theme='dark'] pre { color: whitesmoke; } - /* [data-theme='dark'] label { background-color: #818cab; @@ -382,6 +357,9 @@ a:link, a:visited, a:hover, a:active { border-color: darkcyan; background-color: lightskyblue; } +*/ + +/* .theme-switch-wrapper { display:inline; align-items: center; @@ -390,8 +368,6 @@ a:link, a:visited, a:hover, a:active { font-size: 1rem; } */ - - .theme-switch { display: inline-block; height: 20px; @@ -437,417 +413,3 @@ input:checked + .slider:before { .slider.round:before { border-radius: 50%; } - - - -/* GLFTPD-WEBUI OVERRIDES */ - -:root { - --font-color: #424242; - --bg-color: white; -} -body { - background-color: var(--bg-color); - color: var(--font-color); - margin-left: 6px; - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; -} -iframe { - display: block; - margin: 0 auto; - border: 0; -} -h1 { - text-decoration: underline; - text-decoration-color: lightblue; -} -h6 { - color: black; - background-color: #dfdddd; - padding: 1px; - padding-left: 5px; - margin-top: 1px; - margin-bottom: 8px; -} -h6.out { - margin-left: 16px; - margin-bottom:0px !important; - width: 900px; -} -pre.out { - display: inline-block; - background: black; - color: lightgreen; - width: 900px; - margin-left: 16px; -} -#spy_api_result { - display: none; - margin-left: 16px; - width: auto; -} -pre.about { - display: inline-block; - width: 750px; - height: 460px; - background: gainsboro; - color: darkgreen; - margin: 10px; - padding-left: 150px; - padding-top: 50px; - padding-bottom: 50px; -} -.subcat { - font-size: 1.1rem; - font-weight: 500; - line-height: 1.2; - color: black; - padding: 1px; - padding-left: 5px; - padding-bottom: 3px; - margin-top: 1px; - margin-bottom: 8px; - background-color: #dfdddd; -} -.log_tmpl { - display: block !important; -} -body.log { - width: 90%; -} -h6.log { - margin-bottom:0px !important; - width: auto; - margin-left: 16px; -} -pre.log { - display: inline-block; - background: black; - color: lightgreen; - width: auto; - margin-left: 16px; -} -button, input, optgroup, select, textarea { - border-radius: 6px; -} -.btn-custom { - color: #fff; - background-color: #9b9b9b; - border-color: #9b9b9b; -} -.btn-custom:hover { - color: #ccc; - background-color: #6c757d; - border-color: #6f757d; -} -.btn-back, .btn-top, .btn-end { - left: 85%; - font-size: 1.2em; - background-color :#eaeaeacc; - padding-top: 3px; - padding-bottom: 3px; - padding-left: 5px; - padding-right: 5px; - font-weight: bolder; - color: #007bff; -} -.btn-back { - top: 5%; -} -.btn-top { - bottom: 15%; -} -.btn-end { - bottom: 5%; -} -.btn-top:hover, .btn-end:hover { - color: #0056b3; - text-decoration: underline; -} -.btn-link, .btn-txt { - background: none!important; - border: none; - padding: 0px; - margin: 0px; - font-size: 1rem; - cursor: pointer; -} -.btn-link:hover, .btn-txt:hover { - color: #37374b!important; -} -.btn-txt { - text-decoration: none; -} -.btn-txt:hover { - /* font-weight: bolder; */ - /* text-decoration: underline; */ - background-color: #ccc!important; -} - -.btn-collapse, .btn-refresh { - font-size: 1.25rem; - color: black; - background-color: #b8d7df; - margin-bottom: 25px; - padding: 8px; - border-radius: 6px; - border-color: gray; - border: 10px; -} -.btn-collapse .fa-chevron-up { - display: none; -} -.btn-collapse:hover { - color: white; - background-color: #1f7cd3; -} -.btn-refresh { - font-size: 1rem; - padding: 6px; - background-color: #cfdee2; -} -.btn-refresh:hover, .btn-refresh a:hover { - color: white; -} -.btn-refresh a { - color: inherit; - text-decoration: none; - background-color: inherit; -} -label { - margin-bottom: 0px; - margin-right: 8px; -} -input, button { - margin-left: 5px; -} - -.title { - font-size: 2.5rem; - font-weight: 500; - line-height: 1.2; - margin-top: 15px; - margin-left: 15px; - margin-bottom: 15px; -} - -.status { - margin-top: 15px; - margin-bottom: 35px; - padding-top: 15px; - padding-bottom: 15px; - padding-left: 8px; - border: 2px dashed gray; - border-radius: 15px; - display: inline-block; - width: 1000px; -} -.status a { - color: black; -} -.status a:hover { - color: #0056b3; -} -.status #up, #down, #open, #ts { - display: inline-block; - margin-left: 3px; - margin-right: 3px; - padding-left: 3px; - padding-right: 3px; -} -.status #mode { - display:inline-block; - background-color:#ccc; - /*font-size: 1.25rem;*/ - font-weight: 600; - margin-left: 3px; - margin-right: 3px; - margin-top: 5px; - padding-left: 8px; - padding-right: 8px; -} -.status #label { - display: inline-block; - margin-left: 4px; - margin-right: 0px; - font-size: 1.2em; - text-decoration: underline; -} -.status #up { - background-color: lightgreen; -} -.status #down { - background-color: red; -} -.status #open { - background-color: orange; -} -.status #theme { - display: inline-block; - width: 40px; - height: 20px; - font-size: 0.9em; - line-height: 12px; -} -.status #theme span { - display: inline-flex; - vertical-align: text-bottom; -} - -.status #about { - padding-left: 6px; -} - -.group { - padding: 15px; - border: 1px dotted black; - border-radius: 8px; - width: 900px; - margin-bottom: 40px; -} -.group #auth { - margin-bottom: 50px !important; -} -.group #iptip { - width: 150px; -} -.group #iptip:hover { - border: 2px solid gray; -} -.group #ip { - margin-top: 10px; -} - -.cbview { - border: 2px solid gray; - border-color: orangered; - padding: 15px; - width: 900px; - text-align: center; -} -span.small { - font-size: smaller; -} -hr.vsep { - margin-left: 10px; - margin-right: 10px; - border: none; - border-left: 2px dotted black; - display: inline -} -.hspace { - margin-bottom: 30px; -} -.fa-chevron-right { - margin-right: 12px; -} -.enabled-group { - display:block; - margin-left:16px -} -.disabled-group { - display:none; -} - -.users { - column-count: 2; - column-gap: 0%; - column-span: none; -} -#api_result { - display: none; - position: fixed; - color: white; - background: black; - opacity: 0.9; - padding: 10px; - transform: translate(200px, 150px); -} -#api_result pre { - color: white; -} - -#get_user { - height: unset; - border: 3px solid #ced4da; -} - -.bg-custom { - background-color: #eee; -} - -.color-custom { - color: #069; -} - -.debug { - color: blue; -} - -button .btn-refresh { - background-color: #cad5da; - border-color: #9b9b9b; -} - -.deluser { - cursor: unset!important; -} - -a:focus { - color: purple!important; -} - - -/* THEME SWITCH */ - -.theme-switch { - display: inline-block; - height: 20px; - position: relative; - width: 38px; - margin-bottom: -1px; -} -.theme-switch input { - display:none; -} -.slider { - background-color: #ccc; - bottom: 0; - cursor: pointer; - left: 0; - position: absolute; - right: 0; - top: 0px; - transition: .4s; - margin-bottom:-4px; -} -.slider:before { - background-color: #fff; - bottom: 0px; - content: ""; - height: 16px; - left: 4px; - position: absolute; - transition: .4s; - width: 16px; - top: 4px; -} -input:checked + .slider { - background-color: #66bb6a; - margin-bottom:-4px; -} -input:checked + .slider:before { - transform: translateX(16px); -} -.slider.round { - border-radius: 17px; -} -.slider.round:before { - border-radius: 50%; -} - - -[data-theme='dark'] { - --font-color: #929292; - --bg-color: #161625; -} diff --git a/assets/css/style.css b/assets/css/style.css index ae5829e..7969f30 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -45,7 +45,7 @@ pre.out { } #spy_api_result { - /* display: inline-block */ + display: none; margin-left: 16px; width: auto; border: solid 1px; @@ -53,7 +53,7 @@ pre.out { margin: 10px; padding: 5px; background-color: whitesmoke; - box-shadow: 3px 2px 2px lightgray; + box-shadow: 10px 5px 5px lightgray; } #spy_api_result a { font-weight: bolder; @@ -98,8 +98,8 @@ body.show_txt { } .title.show_txt { - font-size: 20px; width: auto; + font-size: 20px; margin-bottom: 0px !important; margin-left: 16px; padding-left: 6px; @@ -109,10 +109,10 @@ body.show_txt { } pre.show_txt { + width: auto; display: inline-block; background: black; color: lightgreen; - width: auto; margin-left: 16px; padding-left: 8px; } @@ -126,21 +126,48 @@ textarea { } .btn-custom { + color: #000; + background-color: #eee; + border-color: #666; +} + +.btn-custom:hover { + color: #ddd; + background-color: #999; + border-color: #666; +} + +.btn-gray { color: #fff; background-color: #9b9b9b; border-color: #9b9b9b; } -.btn-custom:hover { +.btn-gray:hover { color: #ccc; background-color: #6c757d; border-color: #6f757d; } +.btn-purple { + color: #fff; + background-color: #9063bd; + border-color: #5c4c6b; +} + +.btn-purple:hover { + color: #fff; + background-color: #48137c; + border-color: #5c4c6b; +} + + .btn-back, .btn-top, +.btn-reload, .btn-end { - left: 85%; + left: 70%; + width: fit-content; font-size: 1.2em; background-color: #eaeaeacc; padding-top: 3px; @@ -152,7 +179,11 @@ textarea { } .btn-back { - top: 5%; + top: 6%; +} + +.btn-reload { + top: 1%; } .btn-top { @@ -351,6 +382,33 @@ button { margin-top: 10px; } +.stats { + float: left; + width: 340px; + height: 600px; + background-color:whitesmoke; + margin-bottom: 40px; + margin-right: 90px; + /*padding-left: 10px;*/ +} + +.stats svg { + margin-top: 10px; + padding-left: 5px; + padding-right: 5px; + /* transform: scale(0.9); */ +} + +.stats_main { + margin-left: 10px; + height: 300px; +} + +.stats_tmpl { + width: 80%; + /* height: 1300px; */ +} + .cbview { border: 2px solid gray; border-color: orangered; @@ -456,6 +514,10 @@ a:focus { background-color: var(--bg-color); } +.modal-content { + background-color: white; +} + /* THEME SWITCH */ .theme-switch { @@ -487,7 +549,7 @@ a:focus { bottom: 0px; content: ""; height: 16px; - left: 4px; + left: 3px; position: absolute; transition: .4s; width: 16px; diff --git a/assets/js/modal_func.js b/assets/js/modal_func.js index 9d02b64..1b48510 100644 --- a/assets/js/modal_func.js +++ b/assets/js/modal_func.js @@ -30,7 +30,7 @@ function showModal(title, html) { $('iframe').hide(); $('#bsModalLabel').text(title); $('.modal-body p').show(); - $('.modal-body p').attr("style", "background-color:black"); + $('.modal-body p').attr("class", "modal-content"); $('.modal-body p').html($(parsed_html).text()); //$('.modal-body p').html(parsed_html); }, 100); diff --git a/bin/auth.sh b/bin/auth.sh index f5dc81f..8031653 100755 --- a/bin/auth.sh +++ b/bin/auth.sh @@ -1,5 +1,5 @@ #!/bin/sh -VERSION=V4 +VERSION=V5 ################################## ################################ #### # ## # >> GLFTPD-WEBUI-AUTH ################################## ################################ #### # ## diff --git a/bin/gltool.sh b/bin/gltool.sh index 10cafb8..599a032 100755 --- a/bin/gltool.sh +++ b/bin/gltool.sh @@ -21,10 +21,15 @@ ALLOW_NO_IDENT=1 ALLOW_ALL_IP=1 MIN_IP_OCT="0" MAX_IP="99" +MAX_USERTOP="10" +MAX_GROUPTOP="10" SCRIPTDIR="$(dirname "$(readlink -f -- "$0")")" SCRIPT="$(basename "$0")" +ALL_STATS="DAYUP WKUP MONTHUP ALLUP DAYDN WKDN MONTHDN ALLDN NUKE" +# "GDAYUP GWKUP GMONTHUP GALLUP GDAYDN GWKDN GMONTHDN GALLDN" + # shellcheck disable=SC2016 COMMANDS="$( sed -n '/^case \$COMMAND in/,/^esac/{//d;p;}' "${SCRIPTDIR}/${SCRIPT}" | \ @@ -36,7 +41,7 @@ COMMANDS="$( # shellcheck disable=SC2016 OPTIONS="$( sed -n '/^ *case \$opt in/,/^ *\*) exit 1 && exit 0 ;;/{//d;p;}' "${SCRIPTDIR}/${SCRIPT}" | \ - sed -n 's/ *\([0-9a-z]\)) \(.*\)=.*#\(.*\)$/ -\1 \2\t\3/p' + sed -n 's/ *\([0-9a-z]\)) \(.*\)=.*#\(.*\)$/ [-\1] <\2\> \t\3/p' )" HELP=" @@ -56,7 +61,12 @@ OPTIONS: $OPTIONS -REQUIRES: $REQUIREMENTS + * STATSECTION: <0-9> + * STAT: <${ALL_STATS// /|}> + +EXAMPLE: ${SCRIPT##*/} -c ADDUSER -u MyUser -p secr3t + +REQUIRES:${REQUIREMENTS} " @@ -65,7 +75,7 @@ REQUIRES: $REQUIREMENTS ################################################ OPTIND=1 -while getopts ha:c:d:f:g:i:k:l:u:p:r:s:t:z: opt; do +while getopts ha:c:d:f:g:i:k:l:u:n:p:r:s:t:u::x:z: opt; do case $opt in a) GADMIN=$OPTARG ;; # (ADDUSERGROUP|USERGADMIN) c) COMMAND=$OPTARG ;; @@ -76,11 +86,13 @@ while getopts ha:c:d:f:g:i:k:l:u:p:r:s:t:z: opt; do i) MASK=$OPTARG ;; # (ADDIP|DELIP) k) CREDITS=$OPTARG ;; # (CHCREDITS) l) LOGINS=$OPTARG ;; # (CHLOGINS) - u) USERNAME=$OPTARG ;; # (ADDUSER|DELUSER|AUTH|*IP|*USERGROUP|CH*) + n) STATSECTION=$OPTARG ;; # (USERTOP|GROUPTOP) * p) PASSWORD=$OPTARG ;; # (ADDUSER|CHPASS|AUTH) r) RATIO=$OPTARG ;; # (CHRATIO) s) PGROUP=$OPTARG ;; # (ADDPGROUP|DELPGROUP) t) TAGLINE=$OPTARG ;; # (CHTAG) + u) USERNAME=$OPTARG ;; # (AUTH|ADDUSER|DELUSER|ADDIP|ADDUSERGROUP|CHGRP|CH...) + x) STAT=$OPTARG ;; # (USERTOP|GROUPTOP) * z) ADMIN=$OPTARG ;; *) exit 1 ;; esac @@ -97,7 +109,7 @@ if [ "$(readlink /proc/$$/exe 2>&1)" = "/bin/busybox" ]; then exit 1 fi -# support grep without -P option +# set to 1 for 'grep -P, see func_grep() GREP_PERL=1 if [ "${CHECK_SYS_BINS:-0}" -eq 1 ]; then for i in grep sed cut; do @@ -135,7 +147,7 @@ if [ -n "$USER" ] && [ -n "$FLAGS" ] && [ -n "$GROUP" ]; then fi func_check_ip() { - grep IP "$USERFILE" | grep -F -w -m 1 "$MASK" | cut -d ' ' -f2 + grep IP "$USERFILE" | grep -F -w -m 1 "$MASK" | cut -d ' ' -f2 } func_check_user() { @@ -144,14 +156,41 @@ func_check_user() { echo "ERROR: missing username" exit 1 fi - if [ -d "$GLDIR/ftp-data/users/$USERNAME" ]; then - echo "ERROR: userfile" - exit 1 - fi if [ ! -s "$USERFILE" ]; then echo "ERROR: userfile does not exist" exit 1 fi + if [ -d "$USERFILE" ]; then + echo "ERROR: incorrect userfile" + exit 1 + fi +} + +# grep wrapper +# default: grep '^FOO [^ ]*' in $USERFILE +# supports grep without and with -P option: +# -P, --perl-regexp PATTERNS are Perl regular expressions +func_grep() { + REGEX="[^ ]*" + for a in "$@"; do + if printf -- "%s" "$a" | grep -Eq -- "^-[a-z]"; then + if printf -- "%s" "$a" | grep -Eq -- '^-num'; then + REGEX="([0-9]+ ?)+" + else + ARGS+=" $a " + fi + shift + fi + done + PATTERN="$1" + FILE="${2:-$USERFILE}" + GREP_ARGS="$ARGS --only-matching --word-regexp" + # shellcheck disable=SC2086 + if [ "${GREP_PERL:-1}" -eq 0 ]; then + grep $GREP_ARGS "^${PATTERN} ${REGEX}" "$FILE" | cut -d" " -f2- + else + grep $GREP_ARGS --perl-regexp "^${PATTERN} \K${REGEX}" "$FILE" || true + fi } func_update_userfile() { @@ -251,13 +290,8 @@ func_listusers() { elif [ ! -s "$GLDIR/ftp-data/users/$i" ]; then echo "[error] missing userfile $i" else - if [ "${GREP_PERL:-1}" -eq 0 ]; then - group="$(grep -m1 -ow "^GROUP [^ ]*" "$GLDIR/ftp-data/users/$i" | cut -d" " -f2-)" - flags="$(grep -m1 -ow "^FLAGS .*" "$GLDIR/ftp-data/users/$i" | cut -d" " -f2-)" - else - group="$(grep -m1 -Pow "^GROUP \K[^ ]*" "$GLDIR/ftp-data/users/$i" || true | cut -d" " -f2)" - flags="$(grep -m1 -Pow "^FLAGS \K.*" "$GLDIR/ftp-data/users/$i" || true | cut -d" " -f2)" - fi + group="$(func_grep -m1 GROUP "$GLDIR/ftp-data/users/$i")" + flags="$(func_grep -m1 FLAGS "$GLDIR/ftp-data/users/$i")" if [ -n "$flags" ] && echo "$flags" | grep -q 1; then notes+=" (siteop)" fi @@ -302,11 +336,7 @@ func_rawuserfile() { func_rawuserfilefield() { if [ -n "$USERNAME" ] && [ ! -d "$USERFILE" ] && [ -s "$USERFILE" ] && [ -n "$1" ]; then - if [ "${GREP_PERL:-1}" -eq 0 ]; then - grep -Pow "^$1 \K[^ ]*" "$USERFILE" - else - grep -ow "^$1 [^ ]*" "$USERFILE" | cut -d" " -f2- - fi + func_grep "$1" fi } @@ -336,27 +366,20 @@ func_rawgroups() { func_rawpgroups() { func_get_glconf if [ "${GREP_PERL:-1}" -eq 0 ]; then - grep -ow "^\s*privgroup .*" "$GLCONF" | cut -d" " -f2- | while IFS= read -r i; do - i=$(echo "$i"|sed -e 's/\s\s*/ /g' -e 's|\[:space:\]| |g') - read -r groupname description <<<"$i" - echo "$groupname $description" - done + privgroup="$(grep -ow "^\s*privgroup .*" "$GLCONF" | cut -d" " -f2-)" else - grep -Pow "^\s*privgroup \K.*" "$GLCONF" | while IFS= read -r i; do - i=$(echo "$i"|sed -e 's/\s\s*/ /g' -e 's|\[:space:\]| |g') - read -r groupname description <<<"$i" - echo "$groupname $description" - done + privgroup="$(grep -Pow "^\s*privgroup \K.*" "$GLCONF")" fi + echo "$privgroup" | while IFS= read -r i; do + i=$(echo "$i"|sed -e 's/\s\s*/ /g' -e 's|\[:space:\]| |g') + read -r groupname description <<<"$i" + echo "$groupname $description" + done } func_rawusergroup() { if [ -n "$USERNAME" ] && [ ! -d "$USERFILE" ] && [ -s "$USERFILE" ]; then - if [ "${GREP_PERL:-1}" -eq 0 ]; then - grep -ow "^GROUP \K[^ ]*" "$USERFILE" | cut -d" " -f2- | grep -v "^NoGroup$'" - else - grep -Pow "^GROUP \K[^ ]*" "$USERFILE" | grep -v "^NoGroup$'" - fi + func_grep "GROUP" | grep -v "^NoGroup$'" fi } @@ -364,17 +387,13 @@ func_rawusersgroups() { if [ ! -s "$GLDIR/etc/passwd" ]; then exit 1 fi - cut -d: -f1 < "$GLDIR/etc/passwd" | while IFS= read -r i; do - if [ -s "$GLDIR/ftp-data/users/$i" ]; then - if [ "${GREP_PERL:-1}" -eq 0 ]; then - group="$(grep -m1 -ow "^GROUP [^ ]*" "$GLDIR/ftp-data/users/$i" | cut -d" " -f2-)" - else - group="$(grep -m1 -Pow "^GROUP \K[^ ]*" "$GLDIR/ftp-data/users/$i" || true | cut -d" " -f2)" - fi + cut -d: -f1 < "$GLDIR/etc/passwd" | while IFS= read -r user; do + if [ -s "$GLDIR/ftp-data/users/$user" ]; then + group="$(func_grep -m1 GROUP "$GLDIR/ftp-data/users/$user")" if [ "$group" == "NoGroup" ]; then group="" fi - echo "$i $group" + echo "$user $group" fi done } @@ -385,23 +404,101 @@ func_rawuserspgroups() { fi cut -d: -f1 < "$GLDIR/etc/passwd" | while IFS= read -r i; do if [ -s "$GLDIR/ftp-data/users/$i" ]; then - if [ "${GREP_PERL:-1}" -eq 0 ]; then - pgroup="$(grep -m1 -ow "^PRIVATE [^ ]*" "$GLDIR/ftp-data/users/$i" | cut -d" " -f2-)" - else - pgroup="$(grep -m1 -Pow "^PRIVATE \K[^ ]*" "$GLDIR/ftp-data/users/$i" || true | cut -d" " -f2)" - fi - echo "$i $pgroup" + echo "$i $(func_grep PRIVATE "$GLDIR/ftp-data/users/$i")" fi done } func_rawip() { if [ -n "$USERNAME" ] && [ ! -d "$USERFILE" ] && [ -s "$USERFILE" ]; then - if [ "${GREP_PERL:-1}" -eq 0 ]; then - grep -ow "^IP .*" "$USERFILE" | cut -d" " -f2- + func_grep "IP" + fi +} + +func_rawuserstats() { + if [ -n "$USERNAME" ]; then + for stat in $ALL_STATS; do + func_userstats "$stat" + if [ ! -d "$USERFILE" ] && [ -s "$USERFILE" ]; then + for ((i=0; i < ${#SECTIONS[@]}; i++)); do + echo "$stat $i ${SECTIONS[i]}" + done + fi + done + fi +} + +func_rawusertop() { + if ! echo "$ALL_STATS" | grep -q "$STAT" || [ -z "$STAT" ]; then + exit 1 + fi + bytes=0 + total=0 + for user in $(func_rawusers); do + if [ "${STATSECTION:-0}" -eq 0 ]; then + bytes="$(grep "$STAT" "$GLDIR/ftp-data/users/${user}" | cut -d' ' -f3)" else - grep -Pow "^IP \K.*" "$USERFILE" + USERFILE="$GLDIR/ftp-data/users/${user}" + func_userstats "$STAT" + bytes="$(echo "${SECTIONS[${STATSECTION:-0}]}" | cut -d' ' -f2)" + fi + total=$((total+${bytes:-0})) + result="$result\n$user $bytes" + done + # | sort -k 2 -n -r | head -10 + pos=1 + user="" + sbytes=0 + if [ "$total" -gt 0 ]; then + while read -r line; do + read -r user sbytes <<<"$line" + printf "%s %s %s\n" "$user" "${sbytes:-0}" "$((${sbytes:-0}*100/total))" + done < <(echo -e "$result" | sort -n -r -k2) + fi +} + +func_rawgrouptop() { + if ! echo "$ALL_STATS" | grep -q "$STAT" || [ -z "$STAT" ]; then + exit 1 + fi + total=0 + for groupfile in "$GLDIR"/ftp-data/groups/*; do + group="${groupfile##*/}" + if [ -z "$group" ] || [ "$group" = "default.group" ] || [ "$group" = "NoGroup" ]; then + continue fi + group_bytes=0 + for USERFILE in "$GLDIR"/ftp-data/users/*; do + user="${USERFILE##*/}" + if [ -z "$user" ] || [ "$user" = "default.user" ] || [ "${user##*.}" = ".new" ]; then + continue + fi + user_bytes=0 + if grep -E -m1 "^GROUP " "$USERFILE" | grep -q "^GROUP $group "; then + if [ "${STATSECTION:-0}" -eq 0 ]; then + user_bytes="$(grep "$STAT" "$USERFILE" | cut -d' ' -f3)" + else + func_userstats "$STAT" + user_bytes="$(echo "${SECTIONS[${STATSECTION:-0}]}" | cut -d' ' -f2)" + fi + group_bytes=$((group_bytes+${user_bytes:-0})) + fi + done + total=$((total+${group_bytes:-0})) + result="$result\n$group $group_bytes" + done + pos=1 + group="" + rbytes=0 + if [ "$total" -gt 0 ]; then + while read -r line; do + if [ ${pos:-0} -gt "${MAX_GROUPTOP:-10}" ]; then + break + fi + read -r group rbytes <<<"$line" + printf "%s %s %s\n" "$group" "${rbytes:-0}" "$((${rbytes:-0}*100/total))" + pos="$((pos+1))" + done < <(echo -e "$result" | sort -k2 -n -r) fi } @@ -757,7 +854,7 @@ func_adduser() { fi # shellcheck disable=SC2034 IFS=":" read -r username passwd uid gid date homedir unused <<< "$(tail -1 "$GLDIR/etc/passwd")" - if ! echo "$uid" | grep -Eq '^[0-9]+$'; then + if ! echo "$uid" | grep -Eq '^[0-9]+$'; then echo "ERROR: uid" exit 1 fi @@ -767,7 +864,7 @@ func_adduser() { exit 1 fi HASH="$($GLDIR/bin/hashgen "$USERNAME" "$PASSWORD" | cut -d: -f2)" - if ! echo "$HASH" | grep -Eq '^\$[0-9a-f]{8}\$[0-9a-f]{40}$'; then + if ! echo "$HASH" | grep -Eq '^\$[0-9a-f]{8}\$[0-9a-f]{40}$'; then echo "ERROR: generating hash" exit 1 fi @@ -830,7 +927,7 @@ func_addgroup() { fi # shellcheck disable=SC2034 IFS=":" read -r groupname description gid unused <<< "$(tail -1 "$GLDIR/etc/group")" - if ! echo "$gid" | grep -Eq '^[0-9]+$'; then + if ! echo "$gid" | grep -Eq '^[0-9]+$'; then echo "ERROR: gid" exit 1 fi @@ -900,11 +997,7 @@ func_chflag() { echo "ERROR: missing flag(s)" exit 1 fi - if [ "${GREP_PERL:-1}" -eq 0 ]; then - CURRENT_FLAGS="$( grep -ow "^FLAGS \K.*" "$USERFILE" | cut -d" " -f2-)" - else - CURRENT_FLAGS="$( grep -Pow "^FLAGS \K.*" "$USERFILE" )" - fi + CURRENT_FLAGS="$(func_grep "FLAGS")" NEW_FLAGS="$CURRENT_FLAGS" # shellcheck disable=SC2001 for i in $(echo "$FLAGS" | sed 's/./& /g'); do @@ -976,6 +1069,7 @@ func_delflag() { else NEW_FLAGS="$( grep -Pow "^FLAGS \K.*" "$USERFILE" )" fi + #NEW_FLAGS="$(func_grep "FLAGS")" # shellcheck disable=SC2001 for i in $(echo "$FLAGS" | sed 's/./& /g'); do if echo "$NEW_FLAGS" | grep -q "$i"; then @@ -1114,9 +1208,9 @@ func_bc() { case "$UNIT" in b) RESULT="$1${UNIT}" ;; KB) RESULT="$( echo "$1 1024" | awk '{ printf "%0.0f%s", $1/$2, "KB" }' )" ;; - MB) RESULT="$( echo "$1 1024" | awk '{ printf "%0.1f%s", $1/$2/$2, "MB" }' )" ;; - GB) RESULT="$( echo "$1 1024" | awk '{ printf "%0.1f%s", $1/$2/$2/$2, "GB" }' )" ;; - TB) RESULT="$( echo "$1 1024" | awk '{ printf "%0.2f%s", $1/$2/$2/$2/$2, "TB" }' )" ;; + MB) RESULT="$( echo "$1 1024" | awk '{ printf "%0.1f%s", $1/$2/$2, "MB" }' )" ;; + GB) RESULT="$( echo "$1 1024" | awk '{ printf "%0.1f%s", $1/$2/$2/$2, "GB" }' )" ;; + TB) RESULT="$( echo "$1 1024" | awk '{ printf "%0.2f%s", $1/$2/$2/$2/$2, "TB" }' )" ;; esac fi echo "$RESULT" @@ -1131,101 +1225,161 @@ __func_bc() { } func_userstats() { - COUNT=0 - INDEX=0 - SECTION=() - if [ "${GREP_PERL:-1}" -eq 0 ]; then - while read -d' '-r f; do - case $((COUNT%3)) in - 0) FIELDS=""; FIELDS+="$f " ;; - 1) FIELDS+="$f " ;; - 2) FIELDS+="$f "; SECTION[INDEX]="${FIELDS/% /}" ;; - *) break; - esac - COUNT=$((COUNT+1)) - if [ $((COUNT%3)) -eq 0 ]; then - INDEX=$((INDEX+1)) - fi - done < <(grep -ow "^$1 ([0-9]+ ?)+" "$USERFILE" | cut -d" " -f2-) - else - while read -d' '-r f; do - case $((COUNT%3)) in - 0) FIELDS=""; FIELDS+="$f " ;; - 1) FIELDS+="$f " ;; - 2) FIELDS+="$f "; SECTION[INDEX]="${FIELDS/% /}" ;; - *) break; - esac - COUNT=$((COUNT+1)) - if [ $((COUNT%3)) -eq 0 ]; then - INDEX=$((INDEX+1)) - fi - done < <(grep -Pow "^$1 \K([0-9]+ ?)+" "$USERFILE") - fi -} - -func_rawuserstats() { - for p in DAYUP WKUP MONTHUP ALLUP DAYDN WKDN MONTHDN ALLDN NUKE; do - func_userstats "$p" - for ((i=0; i < ${#SECTION[@]} ; i++)); do - echo "$p $i ${SECTION[i]}" - done - done + count=0 + index=0 + SECTIONS=() + while read -d' '-r f; do + case $((count%3)) in + 0) fields=""; fields+="$f " ;; + 1) fields+="$f " ;; + 2) fields+="$f "; SECTIONS[index]="${fields/% /}" ;; + *) break; + esac + count=$((count+1)) + if [ $((count%3)) -eq 0 ]; then + index=$((index+1)) + fi + done < <(func_grep -num "$1") } func_listuserstats() { + ALL_STATS="MONTHUP" func_check_user "$USERNAME" - printf "PERIOD UP/DN:\tSTAT_SECTION:\t\tBytes / Files:\n" # / Time" - printf -- "------------------------------------------------------------\n" - for p in DAYUP WKUP MONTHUP ALLUP DAYDN WKDN MONTHDN ALLDN; do - func_userstats "$p" - for ((i=0 ; i < ${#SECTION[@]} ; i++)); do - stat_section="$i " - if [ ${i:-255} -eq 0 ]; then - stat_section="$i(DEFAULT)" - fi - if [ $i -eq 0 ] || [ "${SECTION[i]}" != "0 0 0" ]; then - IFS=" " read -r files bytes _time <<<"${SECTION[i]}" - printf "%s\t\t%s\t\t%s/%sf\n" "$p" "$stat_section" "$(func_bc "$bytes")" "$files" + # // Files / Bytes / Time + printf "STATS UP/DN:\tSTAT_SECTION:\t\tBytes / Files:\n%s\n" "$(printf -- "-"%.0s {1..80})" + for stat in ${ALL_STATS//NUKE/}; do + func_userstats "$stat" + for ((i=0 ; i < ${#SECTIONS[@]}; i++)); do + if [ $i -eq 0 ] || [ "${SECTIONS[i]}" != "0 0 0" ]; then + stat_section="$i" + if [ ${i:--1} -eq 0 ]; then + stat_section+="(DEFAULT)" + fi + IFS=" " read -r files bytes _time <<<"${SECTIONS[i]}" + printf "%s\t\t%-09s\t\t%s/%sf\n" "$stat" "$stat_section" "$(func_bc "$bytes")" "$files" fi done echo - done #| sort -k 2n -k 1r - printf "\t\t\t\t\tBytes / Times (Date):\n\n" # / Time" + done + # sort -k 2n -k 1r +} + +func_listusernukes() { + # // Last time nuked / Times nuked / Bytes nuked + printf "\t\t\t\t\tBytes / Times (Date):\n%s\n" "$(printf -- "-"%.0s {1..80})" func_userstats "NUKE" - for ((i=0; i < ${#SECTION[@]} ; i++)); do - if [ "${SECTION[i]}" = "0 0 0" ]; then + for ((i=0; i < ${#SECTIONS[@]}; i++)); do + if [ "${SECTIONS[i]}" = "0 0 0" ]; then continue fi stat_section="$i" - if [ ${i:-255} -eq 0 ]; then - stat_section="$i(DEFAULT)" + if [ ${i:--1} -eq 0 ]; then + stat_section+="(DEFAULT)" fi - IFS=" " read -r last times bytes <<<"${SECTION[i]}" + IFS=" " read -r last times bytes <<<"${SECTIONS[i]}" printf "%s\t\t%-10s\t\t%s %s (%s)\n" "NUKE" "$stat_section" "$(func_bc "$bytes")" "$times" "$(date -d@"$last" +'%F %H:%M')" done echo - if [ "${GREP_PERL:-1}" -eq 0 ]; then - IFS=" " read -r _numlogins lastlogin _maxtime _todaytime <<<"$(grep -ow "^TIME \K([0-9]+ ?)+" "$USERFILE" | cut -d" " -f2-)" - else - IFS=" " read -r _numlogins lastlogin _maxtime _todaytime <<<"$(grep -Pow "^TIME \K([0-9]+ ?)+" "$USERFILE")" - fi +} + +# // Number of times logged in / Last time we saw the user / Max online time a day / Time online today +func_listuserlogins() { + func_check_user "$USERNAME" + IFS=" " read -r numlogins lastlogin maxtime todaytime <<<"$(func_grep -num TIME)" + printf "LOGINS: %s\nMAXTIME: %s\nTODAYTIME: %s\n" "$numlogins" "$maxtime" "$todaytime" if [ -n "$lastlogin" ]; then printf "LAST LOGIN: %s\n" "$(date -d@"$lastlogin" +'%F %H:%M')" fi - echo } func_resetuserstats() { func_check_user "$USERNAME" func_clean_tmp cp "$USERFILE" "$USERFILE.tmp" || { echo "ERROR: updating userfile"; exit 1; } - for p in DAYUP WKUP MONTHUP ALLUP DAYDN WKDN MONTHDN ALLDN NUKE; do - sed -i "s/^$p .*/$p 0 0 0/" "$USERFILE.tmp" + for stat in $ALL_STATS; do + sed -i "s/^$stat .*/$stat 0 0 0/" "$USERFILE.tmp" done || { echo "ERROR: resetting stats"; exit 1; } func_update_userfile func_logmsg "reset stats for \"$USERNAME\"" } +func_usertop() { + bytes=0 + total=0 + if ! echo "$ALL_STATS" | grep -q "$STAT" || [ -z "$STAT" ]; then + STAT="MONTHUP" + fi + for user in $(func_rawusers); do + if [ "${STATSECTION:-0}" -eq 0 ]; then + bytes="$(grep "$STAT" "$GLDIR/ftp-data/users/${user}" | cut -d' ' -f3)" + else + USERFILE="$GLDIR/ftp-data/users/${user}" + func_userstats "$STAT" + bytes="$(echo "${SECTIONS[${STATSECTION:-0}]}" | cut -d' ' -f2)" + fi + total=$((total+${bytes:-0})) + result="$result\n$user $bytes" + done + # | sort -k 2 -n -r | head -10 | nl -w 2 -n rz -s' ' + pos=1 + user="" + rbytes=0 + printf "%s USER TOP:\n%s\n" "$STAT" "$(printf -- "-"%.0s {1..35})" + if [ "$total" -gt 0 ]; then + while read -r line; do + if [ ${pos:-0} -gt "${MAX_USERTOP:-10}" ]; then + break + fi + read -r user rbytes <<<"$line" + printf "%02d %s %s (%s%%)\n" "$pos" "$user" "$(func_bc "${rbytes:-0}")" "$((${rbytes:-0}*100/total))" + pos="$((pos+1))" + done < <(echo -e "$result" | sort -k2 -n -r) + echo + #printf "TOTAL: %s GiB\n\n" "$((total/1024/1024/1024))" + fi +} + +func_grouptop () { + if ! echo "$ALL_STATS" | grep -q "$STAT" || [ -z "$STAT" ]; then + STAT="MONTHUP" + fi + total=0 + for groupfile in "$GLDIR"/ftp-data/groups/*; do + group="${groupfile##*/}" + if [ -z "$group" ] || [ "$group" = "default.group" ] || [ "$group" = "NoGroup" ]; then + continue + fi + group_bytes=0 + for userfile in "$GLDIR"/ftp-data/users/*; do + user="${userfile##*/}" + if [ -z "$user" ] || [ "$user" = "default.user" ] || [ "${user##*.}" = ".new" ]; then + continue + fi + user_bytes=0 + if grep -E -m1 "^GROUP " "$userfile" | grep -q "^GROUP $group "; then + user_bytes="$(grep "$STAT" "$userfile" | cut -d' ' -f3)" + group_bytes=$((group_bytes+${user_bytes:-0})) + fi + done + total=$((total+${group_bytes:-0})) + result="$result\n$group $group_bytes" + done + pos=1 + group="" + rbytes=0 + printf "%s GROUP TOP:\n%s\n" "$STAT" "$(printf -- "-"%.0s {1..35})" + if [ "$total" -gt 0 ]; then + while read -r line; do + if [ ${pos:-0} -gt "${MAX_GROUPTOP:-10}" ]; then + break + fi + read -r group rbytes <<<"$line" + printf "%02d %s %s (%s%%)\n" "$pos" "$group" "$(func_bc "${rbytes:-0}")" "$((${rbytes:-0}*100/total))" + pos="$((pos+1))" + done < <(echo -e "$result" | sort -k2 -n -r) + echo + fi +} ################################################ # COMMANDS @@ -1240,6 +1394,9 @@ case $COMMAND in ;; LISTUSERS) func_listusers && exit 0 ;; LISTGROUPS) func_listgroups && exit 0 ;; + LISTUSERSTATS) func_listuserstats && exit 0 ;; + LISTUSERNUKES) func_listusernukes && exit 0 ;; + LISTUSERLOGINS) func_listuserlogins && exit 0 ;; RAWUSERFILE) func_rawuserfile && exit 0 ;; RAWUSERS) func_rawusers && exit 0 ;; RAWGROUPS) func_rawgroups && exit 0 ;; @@ -1247,9 +1404,13 @@ case $COMMAND in RAWUSERSGROUPS) func_rawusersgroups && exit 0 ;; RAWUSERSPGROUPS) func_rawuserspgroups && exit 0 ;; RAWUSERGROUP) func_rawusergroup && exit 0 ;; - RAWTAG) func_rawuserfilefield "TAGLINE" && exit 0 ;; + RAWIP) func_rawip && exit 0 ;; RAWFLAG) func_rawuserfilefield "FLAGS" && exit 0 ;; RAWCREDS) func_rawuserfilefield "CREDITS" && exit 0 ;; + RAWTAG) func_rawuserfilefield "TAGLINE" && exit 0 ;; + RAWUSERSTATS) func_rawuserstats && exit 0 ;; + RAWUSERTOP) func_rawusertop && exit 0 ;; + RAWGROUPTOP) func_rawgrouptop && exit 0 ;; DELUSER) func_deluser && exit 0 ;; ADDGROUP) func_addgroup && exit 0 ;; DELGROUP) func_delgroup && exit 0 ;; @@ -1261,7 +1422,6 @@ case $COMMAND in USERGADMIN) func_usergadmin && exit 0 ;; CHGADMIN) func_chgadmin && exit 0 ;; LISTIP) func_listip && exit 0 ;; - RAWIP) func_rawip && exit 0 ;; ADDIP) func_addip && exit 0 ;; DELIP) func_delip && exit 0 ;; CHPASS) func_chpass && exit 0 ;; @@ -1272,10 +1432,10 @@ case $COMMAND in CHRATIO) func_chratio && exit 0 ;; ADDFLAG) func_addflag && exit 0 ;; DELFLAG) func_delflag && exit 0 ;; - RAWUSERSTATS) func_rawuserstats && exit 0 ;; - LISTUSERSTATS) func_listuserstats && exit 0 ;; RESETUSERSTATS) func_resetuserstats && exit 0 ;; LOGTAIL) func_logtail ;; LOGSHOW) func_logshow ;; + USERTOP) func_usertop && exit 0 ;; + GROUPTOP) func_grouptop && exit 0 ;; *) echo "ERROR: no such cmd"; exit 1 ;; esac diff --git a/docker-build.sh b/docker-build.sh index 2959520..89d5b54 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -1,5 +1,5 @@ #!/bin/bash -VERSION=V4 +VERSION=V5 ################################## ################################ #### # ## # >> DOCKER-BUILD-GLFTPD :: WEBUI ################################## ################################ #### # ## diff --git a/docker-compose.yml b/docker-compose.yml index b323a7e..e8226e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ --- ################################################################### #### # ## -# >> DOCKER-COMPOSE-GLFTPD-V4 :: WEBUI +# >> DOCKER-COMPOSE-GLFTPD-V5 :: WEBUI ################################################################### #### # ## # build args: # environment options: @@ -15,27 +15,27 @@ services: - source: ./glftpd/site target: /glftpd/site type: bind - bind: { create_host_path: false } + bind: {create_host_path: false} - source: ./glftpd/glftpd.conf target: /glftpd/glftpd.conf type: bind - bind: { create_host_path: false } + bind: {create_host_path: false} - source: ./glftpd/etc target: /glftpd/etc type: bind - bind: { create_host_path: false } + bind: {create_host_path: false} - source: ./glftpd/ftp-data/users target: /glftpd/ftp-data/users type: bind - bind: { create_host_path: false } + bind: {create_host_path: false} - source: ./glftpd/ftp-data/groups target: /glftpd/ftp-data/groups type: bind - bind: { create_host_path: false } + bind: {create_host_path: false} - source: ./glftpd/sitebot target: /glftpd/sitebot type: bind - bind: { create_host_path: false } + bind: {create_host_path: false} ports: &glftpd_ports - "1337:1337" # uncomment for bot @@ -43,7 +43,7 @@ services: # uncomment for pasv ports # - "5000-6000:5000-6000" ulimits: &glftpd_ulimits - nofile: { soft: 1024, hard: 1024 } + nofile: {soft: 1024, hard: 1024} container_name: glftpd hostname: glftpd # environment: @@ -59,7 +59,7 @@ services: ports: &web_ports - 4444:443 ulimits: &web_ulimits - nofile: { soft: 1024, hard: 1024 } + nofile: {soft: 1024, hard: 1024} container_name: glftpd-web hostname: glftpd-web depends_on: [glftpd] @@ -76,8 +76,8 @@ services: dockerfile: Dockerfile args: # if glftpd.io is down, uncomment to use mirror - # - GLFTPD_URL=https://mirror.glftpd.nl.eu.org/files/glftpd-LNX-2.14a_3.0.12_x64.tgz - - GLFTPD_SHA=981fec98d3c92978f8774a864729df0a2bca91afc0672c51833f0cfc10ac04935ccaadfe9798a02711e3a1c4c714ddd75d5edd5fb54ff46ad495b1a2c391c1ad + # - GLFTPD_URL=https://mirror.glftpd.nl.eu.org/files/glftpd-LNX-2.15_3.4.0_x64.tgz + - GLFTPD_SHA=a9ce10867aed6a377c7d47864d59668a433956fba1998acc8bf8d6f16c06870143c66b987586281d65e1fe99422fe57ef99fbc71bc62bbd34448b1a4af24264b - INSTALL_ZS=1 - INSTALL_BOT=1 - INSTALL_WEBUI=1 diff --git a/docker-run.sh b/docker-run.sh index b9e6a84..9b54ce1 100755 --- a/docker-run.sh +++ b/docker-run.sh @@ -1,5 +1,5 @@ #!/bin/bash -VERSION=V4 +VERSION=V5 ################################## ################################ #### # ## # >> DOCKER-RUN-GLFTPD :: WEBUI ################################## ################################ #### # ## diff --git a/docs/Config.md b/docs/Config.md new file mode 100644 index 0000000..e3c8f78 --- /dev/null +++ b/docs/Config.md @@ -0,0 +1,111 @@ +# Config Settings + +### mode + +docker + +- api: `` +- glftpd_ct_name `` (default: glftpd) +- bin_dir: `` to binaries * + +local + +- runas: `` for sudo (default: '/usr/bin/sudo -n -u root') +- bin_dir: `` to binaries + +_* path to gltool.sh, gotty, pywho etc_ + + +### display + +show_alerts + + - `true` show notification + - `false` no notifications + +show_more_options + + - `true` always show all options (default) + - `false` collapse with link "Show/hide more options" + +max_items: `` of users and groups to show (without collapse) + +modal + + - commands: + - `true` show (terminal) commands in dialog, `false` outputs to bottom of page + - sitewho: + - `true` show pywho in dialog, `false` outputs to bottom of page + - userstats: + - `true` show userstats in dialog, `false` outputs to bottom of page + +spy + + - show: `true` show online users, `false` hide online users + - refresh: `true` auto refresh, `false` no refresh + +theme + +- title: `` + +- button colors * + - btn-color-1: `` and `` + - btn-color-2: `<...>` + - btn-small-color: `<...>` + +*\* for theme button colors see https://getbootstrap.com/docs/4.0/components/buttons/* +\* *plus these extra colors: 'custom', 'purple', 'gray'* + + +### connections + +services + +- glftpd: + - host: `` (default: localhost), port: `` (default: 1337) +- sitebot: + - host: `` (default: localhost), port: `` (default: 3333) + +*(used for UP/DOWN status)* + +### filemanager + +filemanager * + +- (Browse) Name: + - type: 'dir', path: "/path/to/dir" +- (Edit/View) filename: + - type: 'file', mode: ``, path: "/path/to/filename/" + +default file mode is 'edit' + +if there are no dirs/files defined, filemanager tab is not shown + +_* if path is empty, defaults for mode is used (docker/local)_ + +### ui buttons + +buttons + +- Glftpd + - name: `` +- Docker + - name: `` +- Terminal + - name: `` + +add separator(s) with `'sep'` + +*(if there are no buttons defined, it's tab is not shown)* + +### stats + +- commands + - cmd: ``, stat: ``, show: `<0=hide|1=stats-page|2=main>` +- options + +- palette + - name + - `<#color-1>` + - `<#color-2>` + - `<#color-3>` diff --git a/docs/Setup.md b/docs/Setup.md index 77e6e0d..8d9e3d6 100644 --- a/docs/Setup.md +++ b/docs/Setup.md @@ -45,95 +45,17 @@ Or, to set this up completely manually ## Settings -These can be changed in config.php +Initial settings are copied from 'config.php.dist' to 'config.php', where they can be changed using filemanager. -#### mode +If you change auth mode in "login" page, it automatically changes config. Same if you run auth.sh. -docker - -- api: `` -- glftpd_ct_name `` (default: glftpd) -- bin_dir: `` to binaries * - -local - -- runas: `` for sudo (default: '/usr/bin/sudo -n -u root') -- bin_dir: `` to binaries - -_* path to gltool.sh, gotty, pywho etc_ - - -#### display - -show_more_opts - - - `true` always show all options - - `false` collapse with link "Show/hide more options" - -show_alerts - - - `true` show notifications, `false` no notifications - -max_items: `` of users and groups to show (without collapse) - -modal - - - commands: - - `true` show (terminal) commands in dialog, `false` outputs to bottom of page - - sitewho: - - `true` show pywho in dialog, `false` outputs to bottom of page - -spy - - - enabled: `true` show online users, `false` hide online users - - refresh: `true` auto refresh, `false` no refresh - -#### connections - -services - -- glftpd: - - host: `` (default: localhost), port: `` (default: 1337) -- sitebot: - - host: `` (default: localhost), port: `` (default: 3333) - -*(used for UP/DOWN status)* - -### filemanager - -filemanager * - -- (Browse) Name: - - type: 'dir', path: "/path/to/dir" -- (Edit/View) filename: - - type: 'file', mode: ``, path: "/path/to/filename/" - -default file mode is 'edit' - -if there are no dirs/files defined, filemanager tab is not shown - -_* if path is empty, defaults for mode is used (docker/local)_ - -#### ui buttons - -buttons - -- Glftpd - - name: `` -- Docker - - name: `` -- Terminal - - name: `` - -add separator(s) with `'sep'` - -*(if there are no buttons defined, it's tab is not shown)* +For all options see [Config.md](Config.md) ## Details webui-mode -More info about different setup modes +More info about different setup modes: -### docker +#### docker Commands: docker_commands.php, uses docker api @@ -147,7 +69,7 @@ Notes: - to use a different image, change it in `docker-run.sh` - to not start gl at all set `GLFTPD=0`, for example if you already have your own glftpd container running -### local +#### local Enabled if `WEBUI_LOCAL=1` is set on runtime diff --git a/lib/svg-chart-builder/.gitignore b/lib/svg-chart-builder/.gitignore new file mode 100644 index 0000000..206e4fb --- /dev/null +++ b/lib/svg-chart-builder/.gitignore @@ -0,0 +1,3 @@ +.idea +.phpunit.result.cache +/vendor diff --git a/lib/svg-chart-builder/CHANGELOG.md b/lib/svg-chart-builder/CHANGELOG.md new file mode 100644 index 0000000..eb863bc --- /dev/null +++ b/lib/svg-chart-builder/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog +All notable changes to this project will be documented in this file. + + +## [1.0.0] + +- Initial version released. diff --git a/lib/svg-chart-builder/CONTRIBUTING.md b/lib/svg-chart-builder/CONTRIBUTING.md new file mode 100644 index 0000000..b4ae1c4 --- /dev/null +++ b/lib/svg-chart-builder/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/lib/svg-chart-builder/LICENSE.md b/lib/svg-chart-builder/LICENSE.md new file mode 100644 index 0000000..244229e --- /dev/null +++ b/lib/svg-chart-builder/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/svg-chart-builder/README.md b/lib/svg-chart-builder/README.md new file mode 100644 index 0000000..8ecb123 --- /dev/null +++ b/lib/svg-chart-builder/README.md @@ -0,0 +1,241 @@ +# SVGChartBuilder + +SVGChartBuilder is a PHP library that allows you to generate SVG-based charts for use in your backend applications. + +### Requirements + +PHP 8.0 or later + +## Installation + +You can install SVGChartBuilder via Composer: + +To install using Composer, run the following command in your terminal: + +```bash + composer require xanpena/svg-chart-builder +``` + +## Usage + +SVGChartBuilder provides several types of charts that you can create: + +### Example + +To create a bar chart, use the following code: + +- Tipo Bar chart +```php +use Xanpena\SVGChartBuilder\SVGChartBuilder; + +$data = [ + 16, + 18, + 40, + // ... other data ... +]; + +$options = [ + 'labels' => [ + 'math', + 'literature', + 'english', + // ... other data ... + ], + 'colors' => [ + '#CDDC39', + '#00BCD4', + '#9E9E9E', + // ... other data ... + ], + 'axisColors' => [ + 'x' => 'red', + 'y' => 'blue' + ], + 'labelsColor' => 'orange', + 'dataColor' => 'white', +]; + +$chartBuilder = new SVGChartBuilder(SVGChartBuilder::CHART_TYPE_BAR, $data, $options); +$svg = $chartBuilder->create(); +echo $svg; +``` + +- Tipo Horizontal bar chart +```php +use Xanpena\SVGChartBuilder\SVGChartBuilder; + +$data = [ + 16, + 18, + 40, + // ... other data ... +]; + +$options = [ + 'labels' => [ + 'math', + 'literature', + 'english', + // ... other data ... + ], + 'colors' => [ + '#CDDC39', + '#00BCD4', + '#9E9E9E', + // ... other data ... + ], + 'axisColors' => [ + 'x' => 'red', + 'y' => 'blue' + ], + 'labelsColor' => 'orange', + 'dataColor' => 'white', +]; + +$chartBuilder = new SVGChartBuilder(SVGChartBuilder::CHART_TYPE_HORIZONTALBAR, $data, $options); +$svg = $chartBuilder->create(); +echo $svg; +``` + +- Tipo Doughnut chart +```php +use Xanpena\SVGChartBuilder\SVGChartBuilder; + +$data = [ + 16, + 18, + 40, + // ... other data ... +]; + +$options = [ + 'labels' => [ + 'math', + 'literature', + 'english', + // ... other data ... + ], + 'colors' => [ + '#CDDC39', + '#00BCD4', + '#9E9E9E', + // ... other data ... + ], + 'labelsColor' => 'white' +]; + +$chartBuilder = new SVGChartBuilder(SVGChartBuilder::CHART_TYPE_DOUGHNUT, $data, $options); +$svg = $chartBuilder->create(); +echo $svg; +``` + +- Tipo Pie chart +```php +use Xanpena\SVGChartBuilder\SVGChartBuilder; + +$data = [ + 16, + 18, + 40, + // ... other data ... +]; + +$options = [ + 'labels' => [ + 'math', + 'literature', + 'english', + // ... other data ... + ], + 'colors' => [ + '#CDDC39', + '#00BCD4', + '#9E9E9E', + // ... other data ... + ], + 'labelsColor' => 'white' +]; + +$chartBuilder = new SVGChartBuilder(SVGChartBuilder::CHART_TYPE_PIE, $data, $options); +$svg = $chartBuilder->create(); +echo $svg; +``` + +- Tipo Line chart +```php +use Xanpena\SVGChartBuilder\SVGChartBuilder; + +$data = [ + 'math' => [ + 11, + 17, + 15, + // ... other data ... + ], + 'literature' => [ + 21, + 21, + 23, + // ... other data ... + ], + 'english' => [ + 14, + 9, + 18, + // ... other data ... + ] + // ... other data ... +]; + +$options = [ + 'labels' => [ + '2020/2021', + '2021/2022', + '2023/2024', + // ... other data ... + ], + 'colors' => [ + '#CDDC39', + '#00BCD4', + '#9E9E9E', + // ... other data ... + ], + 'axisColors' => [ + 'x' => 'red', + 'y' => 'blue' + ], + 'labelsColor' => 'orange', +]; + +$chartBuilder = new SVGChartBuilder(SVGChartBuilder::CHART_TYPE_LINE, $data, $options); +$svg = $chartBuilder->create(); +echo $svg; +``` + +### Chart Types +SVGChartBuilder supports the following chart types: + +SVGChartBuilder::BAR_CHART: Bar chart
+SVGChartBuilder::HORIZONTALBAR_CHART: Horizontal bar chart
+SVGChartBuilder::DOUGHNUT_CHART: Doughnut chart
+SVGChartBuilder::PIE_CHART: Pie chart
+SVGChartBuilder::CHART_TYPE_LINE: Line chart
+ + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +### Security + +If you discover any security related issues, please using the issue tracker. + +## Credits + +- [Xan Pena](https://github.com/xanpena) +- [Francisco Prieto](https://github.com/fjpj2310) diff --git a/lib/svg-chart-builder/TODO.md b/lib/svg-chart-builder/TODO.md new file mode 100644 index 0000000..c3a87ef --- /dev/null +++ b/lib/svg-chart-builder/TODO.md @@ -0,0 +1,9 @@ +# Cosas por hacer + +- Implementar tipo line. HECHO +- Refactorizar para que no se repita codigo. HECHO +- Corregir Horizontalbar. HECHO +- Poner el blanco en medio al tipo donuts. HECHO +- Añadir que los colores sean configurables. HECHO +- Configurar colores de los ejes y de los labels. HECHO +- Implementar que los ejes muestren los valores segun los datos y no el porcentaje. HECHO diff --git a/lib/svg-chart-builder/composer.json b/lib/svg-chart-builder/composer.json new file mode 100644 index 0000000..70963db --- /dev/null +++ b/lib/svg-chart-builder/composer.json @@ -0,0 +1,32 @@ +{ + "name": "xanpena/svg-chart-builder", + "description": "PHP SVG Chart Builder", + "keywords": [ + "php", + "svg", + "charts", + "pdf" + ], + "type": "library", + "license": "MIT", + "autoload": { + "psr-4" : { + "Xanpena\\SVGChartBuilder\\": "src" + } + }, + "autoload-dev": { + "psr-4" : { + "Xanpena\\SVGChartBuilder\\Tests\\" : "tests" + } + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-coverage": "vendor/bin/phpunit --coverage-html coverage" + } +} diff --git a/lib/svg-chart-builder/composer.lock b/lib/svg-chart-builder/composer.lock new file mode 100644 index 0000000..2cd0578 --- /dev/null +++ b/lib/svg-chart-builder/composer.lock @@ -0,0 +1,1753 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "be9f3e4553e199704670362ff37c5410", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.17.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + }, + "time": "2023-08-13T19:53:39+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.27", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1", + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.15", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-07-26T13:44:30+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.11", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "810500e92855eba8a7a5319ae913be2da6f957b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/810500e92855eba8a7a5319ae913be2da6f957b0", + "reference": "810500e92855eba8a7a5319ae913be2da6f957b0", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.11" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-08-19T07:10:56+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-07T05:35:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bde739e7565280bda77be70044ac1047bc007e34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-02T09:26:13+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.0" + }, + "platform-dev": [], + "plugin-api-version": "2.1.0" +} diff --git a/lib/svg-chart-builder/examples/bar.svg b/lib/svg-chart-builder/examples/bar.svg new file mode 100644 index 0000000..65348a8 --- /dev/null +++ b/lib/svg-chart-builder/examples/bar.svg @@ -0,0 +1 @@ +04812162024283236401618402512161840mathliteratureenglishtechnologymusicfrenchspanishphysics diff --git a/lib/svg-chart-builder/examples/doughnut.svg b/lib/svg-chart-builder/examples/doughnut.svg new file mode 100644 index 0000000..8d1d064 --- /dev/null +++ b/lib/svg-chart-builder/examples/doughnut.svg @@ -0,0 +1 @@ +math (16)literature (18)english (40)technology (25)music (12)french (16)spanish (18)physics (40) diff --git a/lib/svg-chart-builder/examples/horizontal-bar.svg b/lib/svg-chart-builder/examples/horizontal-bar.svg new file mode 100644 index 0000000..68fb436 --- /dev/null +++ b/lib/svg-chart-builder/examples/horizontal-bar.svg @@ -0,0 +1 @@ +16184025121618400481216202428323640mathliteratureenglishtechnologymusicfrenchspanishphysics diff --git a/lib/svg-chart-builder/examples/line.svg b/lib/svg-chart-builder/examples/line.svg new file mode 100644 index 0000000..d7e08d4 --- /dev/null +++ b/lib/svg-chart-builder/examples/line.svg @@ -0,0 +1 @@ +02.34.66.99.211.513.816.118.420.723111715212123149182020/20212021/20222023/2024mathliteratureenglish diff --git a/lib/svg-chart-builder/examples/pie.svg b/lib/svg-chart-builder/examples/pie.svg new file mode 100644 index 0000000..5fa5d62 --- /dev/null +++ b/lib/svg-chart-builder/examples/pie.svg @@ -0,0 +1 @@ +math (16)literature (18)english (40)technology (25)music (12)french (16)spanish (18)physics (40) diff --git a/lib/svg-chart-builder/src/SVGChartBuilder.php b/lib/svg-chart-builder/src/SVGChartBuilder.php new file mode 100644 index 0000000..386be2d --- /dev/null +++ b/lib/svg-chart-builder/src/SVGChartBuilder.php @@ -0,0 +1,261 @@ +validateType($type); + $this->validateData($type, $data); + $this->validateOptions($type, $data, $options); + + $this->type = $type; + $this->data = $data; + $this->options = $options; + } + + /** + * Create and return the SVG chart. + * + * @return string SVG representation of the chart. + */ + public function create() + { + $chart = ''; + + switch ($this->type) { + case self::CHART_TYPE_BAR: + $chart = (new BarChartBuilder($this->data, $this->options))->makeSvg(); + break; + case self::CHART_TYPE_DOUGHNUT: + $chart = (new DoughnutChartBuilder($this->data, $this->options))->makeSvg(); + break; + case self::CHART_TYPE_HORIZONTALBAR: + $chart = (new HorizontalBarChartBuilder($this->data, $this->options))->makeSvg(); + break; + case self::CHART_TYPE_PIE: + $chart = (new PieChartBuilder($this->data, $this->options))->makeSvg(); + break; + case self::CHART_TYPE_LINE: + $chart = (new LineChartBuilder($this->data, $this->options))->makeSvg(); + break; + } + + return $chart; + } + + /** + * Validate the chart type. + * + * @param string $type Type of chart to validate. + * @throws \InvalidArgumentException If the type is not valid. + */ + protected function validateType($type) + { + if (!in_array($type, $this->validChartTypes)) { + throw new \InvalidArgumentException("Invalid chart type: $type"); + } + } + + /** + * Validate the chart data. + * + * @param string $type Type of chart to validate. + * @param array $data Data for the chart to validate. + * @throws \InvalidArgumentException If the data is not valid. + */ + protected function validateData($type, $data) + { + if (!is_array($data) || empty($data)) { + throw new \InvalidArgumentException("Data must be a non-empty array"); + } + + switch ($type) { + case self::CHART_TYPE_LINE: + $this->validateDataLineGraph($data); + break; + default: + $this->validateDataGenericGraph($data); + break; + } + } + + + protected function validateDataGenericGraph($data) + { + foreach ($data as $value) { + if (!is_numeric($value)) { + throw new \InvalidArgumentException("Each element of data must have a numeric value"); + } + } + } + + + protected function validateDataLineGraph($data) + { + $firstNumElements = null; + + foreach ($data as $values) { + if (!is_array($values)) { + throw new \InvalidArgumentException("Each element of data must have an array of numerical values."); + } + + $secondNumElements = count($values); + if ($firstNumElements === null) { + $firstNumElements = $secondNumElements; + } else if ($firstNumElements !== $secondNumElements) { + throw new \InvalidArgumentException("Each element of data must have the same number of elements."); + } + + foreach ($values as $value) { + if (!is_numeric($value)) { + throw new \InvalidArgumentException("Each element of data must have an array of numerical values."); + } + } + } + } + + protected function validateOptions($type, $data, $options) + { + if (empty($options) === false) { + if (!is_array($options)) { + throw new \InvalidArgumentException("Options must be a array"); + } + + foreach ($options as $option => $values) { + switch ($option) { + case self::OPTION_WIDTH: + case self::OPTION_HEIGHT: + if (!is_numeric($values)) { + throw new \InvalidArgumentException("The $option option must be a numerical"); + } + break; + case self::OPTION_LABELS: + switch ($type) { + case self::CHART_TYPE_LINE: + if (count(array_values($data)[0]) !== count($values)) { + throw new \InvalidArgumentException("The $option option must have the same number of data elements"); + } + break; + default: + if (count($data) !== count($values)) { + throw new \InvalidArgumentException("The $option option must have the same number of data elements"); + } + break; + } + break; + case self::OPTION_COLORS: + switch ($type) { + case self::CHART_TYPE_LINE: + if (count(array_keys($data)) !== count($values)) { + throw new \InvalidArgumentException("The $option option must have the same number of data elements"); + } + break; + default: + if (count($data) !== count($values)) { + throw new \InvalidArgumentException("The $option option must have the same number of data elements"); + } + break; + } + break; + case self::OPTION_AXIS_COLORS: + if (array_keys($values) !== ["x", "y"]) { + throw new \InvalidArgumentException("The $option must be an array with the keys x and y and their respective colors"); + } + break; + case self::OPTION_BANNER_INFO: + if (!is_bool($values)) { + throw new \InvalidArgumentException("The $option option must be a boolean"); + } + break; + case self::OPTION_POINTS_COLOR: + case self::OPTION_DATA_TEXT: + switch ($type) { + case self::CHART_TYPE_LINE: + if (!is_array($values)) { + throw new \InvalidArgumentException("Each element of $option must have an array of values."); + } else if (count($data) !== count($values)) { + throw new \InvalidArgumentException("Each element of $option must have the same number of elements than data"); + } + + foreach ($data as $key => $value) { + if (array_key_exists($key, $values) === false) { + throw new \InvalidArgumentException("Each element of $option must have the same keys than data"); + } else if (count($value) !== count($values[$key])) { + throw new \InvalidArgumentException("Each element of $option must have the same number of elements than data"); + } + } + break; + default: + break; + } + break; + default: + if (!is_string($values) && !is_numeric($values)) { + throw new \InvalidArgumentException("The $option option must be a string or a numeric"); + } + break; + } + } + } + } + +} diff --git a/lib/svg-chart-builder/src/Svg/BarChartBuilder.php b/lib/svg-chart-builder/src/Svg/BarChartBuilder.php new file mode 100644 index 0000000..8d30377 --- /dev/null +++ b/lib/svg-chart-builder/src/Svg/BarChartBuilder.php @@ -0,0 +1,181 @@ +width = max($this->width, 100 + (count($this->data) * 30)); + + $this->openSvgTag() + ->drawSeries() + ->drawGraphData() + ->drawLabels() + ->drawAxis() + ->closeSvgTag(); + + return $this->svg; + } + + /** + * Generate the X and Y axes of the bar chart. + * + * @return $this + */ + protected function drawAxis() + { + $this->svg .= ''; + $this->svg .= ''; + + return $this; + } + + /** + * Generate the chart on SVG canvas. + * + * @return $this + */ + protected function drawGraphData() + { + $numData = count($this->data); + $availableWidth = $this->width - 100; + $widthRatio = $availableWidth / $numData; + $spaceRatio = max(5, min(10, 40 / $numData)); + + $baseX = 50; + $baseY = 250; + + $maxValue = max($this->data); + $maxValue = ($maxValue != 0) ? $maxValue : 1; + + $counter = 0; + $x = $baseX; + + foreach ($this->data as $data) { + if ($counter >= count($this->colors)) { + $counter = 0; + } + + $proportion = $data / $maxValue; + $barHeight = $proportion * ($baseY - 50); + $y = $baseY - $barHeight; + + $this->svg .= ''; + + $valueX = $x + $widthRatio / 2; + $valueY = $y + $barHeight / 2; + + $this->svg .= '' . $data . ''; + + $x += $widthRatio + $spaceRatio; + $counter++; + } + + return $this; + } + + /** + * Generate the labels below the X axis of the chart. + * + * @return $this + */ + protected function drawLabels() + { + $numData = count($this->data); + $availableWidth = $this->width - 100; + $widthRatio = $availableWidth / $numData; + $spaceRatio = max(5, min(10, 40 / $numData)); + + $baseX = 50; + $baseY = 250; + + $x = $baseX + $widthRatio / 2; + $y = $baseY + 30; + + $rotation = -45; + $verticalOffset = 10 * abs(sin(deg2rad($rotation))); + $horizontalOffset = -($widthRatio / 4); + + foreach ($this->labels as $key => $label) { + if ($key >= count($this->colors)) { + $key = 0; + } + + $this->svg .= ''.$label.''; + + $x += $widthRatio + $spaceRatio; + } + + return $this; + } + + /** + * Generate the Y axis labels. + * + * @return $this + */ + protected function drawSeries() + { + $numTicks = $this->calculateNumTicks(); + $maxValue = max($this->data); + $maxValue = (($maxValue != 0) ? $maxValue : 1); + $interval = $maxValue / ($numTicks - 1); + + $baseX = 40; + $baseY = 250; + + $x = $baseX - 10; + + for ($i = 0; $i < $numTicks; $i++) { + $tickValue = $i * $interval; + + if ($tickValue != (int) $tickValue) { + $tickValue = round($tickValue, 2); + } + + $barHeight = ($tickValue / $maxValue) * ($baseY - 50); + $y = $baseY - $barHeight; + + $this->svg .= ''; + + $labelY = $y + 5; + $this->svg .= ''.$tickValue.''; + } + + return $this; + } + + protected function calculateNumTicks() + { + $maxValue = max($this->data); + $minValue = 0; + $range = $maxValue - $minValue; + $minInterval = 1; + + $numTicks = max(2, ceil($range / $minInterval) + 1); + $numTicks = min($numTicks, 11); + + return $numTicks; + } + + protected function getAxisColor($axis) + { + if (empty($this->axisColors) === false && array_key_exists($axis, $this->axisColors)) { + return $this->axisColors[$axis]; + } + + return '#000000'; + } + +} diff --git a/lib/svg-chart-builder/src/Svg/BaseChartBuilder.php b/lib/svg-chart-builder/src/Svg/BaseChartBuilder.php new file mode 100644 index 0000000..7107b14 --- /dev/null +++ b/lib/svg-chart-builder/src/Svg/BaseChartBuilder.php @@ -0,0 +1,103 @@ +data = $data; + $this->configureOptions($options); + + $this->initialize($data); + } + + /** + * Generate the SVG representation of the chart. + * + * @return string The SVG representation of the chart. + */ + abstract protected function makeSvg(); + + /** + * Generate the chart on SVG canvas. + * + * @return $this + */ + abstract protected function drawGraphData(); + + /** + * Generate the labels of the chart. + * + * @return $this + */ + abstract protected function drawLabels(); + + + /** + * Initialize properties. + * + * @return void + */ + protected function initialize($data) { + // Method to be overridden if necessary + } + + + /** + * Open the SVG tag with the specified width and height. + * + * @return $this + */ + protected function openSvgTag() + { + $this->svg = ''; + + return $this; + } + + /** + * Close the SVG tag. + * + * @return $this + */ + protected function closeSvgTag() + { + $this->svg .= ''; + + return $this; + } + + protected function configureOptions($options) + { + foreach ($options as $option => $values) { + if (in_array($option, SVGChartBuilder::OPTION_TYPES) && property_exists($this, $option)) { + $this->{$option} = $options[$option]; + } + } + } + +} diff --git a/lib/svg-chart-builder/src/Svg/DoughnutChartBuilder.php b/lib/svg-chart-builder/src/Svg/DoughnutChartBuilder.php new file mode 100644 index 0000000..1675446 --- /dev/null +++ b/lib/svg-chart-builder/src/Svg/DoughnutChartBuilder.php @@ -0,0 +1,110 @@ +openSvgTag() + ->drawGraphData() + ->drawLabels() + ->closeSvgTag(); + + return $this->svg; + } + + /** + * Draw the slices of the pie chart based on the data. + * + * @return $this + */ + protected function drawGraphData() + { + $totalValue = array_sum($this->data); + $startAngle = 0; + $counter = 0; + + foreach ($this->data as $value) { + if ($value <= 0) { + continue; + } + + if ($counter >= count($this->colors)) { + $counter = 0; + } + + $proportion = $value / $totalValue; + $endAngle = $startAngle + ($proportion * 360); + + $startX = $this->width / 2; + $startY = $this->height / 2; + + $endX1 = $startX + cos(deg2rad($startAngle)) * ($this->width / 2); + $endY1 = $startY + sin(deg2rad($startAngle)) * ($this->height / 2); + + $endX2 = $startX + cos(deg2rad($endAngle)) * ($this->width / 2); + $endY2 = $startY + sin(deg2rad($endAngle)) * ($this->height / 2); + + $largeArcFlag = $proportion > 0.5 ? 1 : 0; + + $this->svg .= ''; + + $startAngle = $endAngle; + $counter++; + } + + $centerX = $this->width / 2; + $centerY = $this->height / 2; + $circleRadius = $this->innerRadius * 0.8; + + $this->svg .= ''; + + return $this; + } + + /** + * Draw the labels for each slice on the chart. + * + * @return $this + */ + protected function drawLabels() + { + $totalValue = array_sum($this->data); + $startAngle = 0; + + foreach ($this->data as $index => $value) { + if ($value <= 0) { + continue; + } + + $label = $value; + if (array_key_exists($index, $this->labels)) { + $label = $this->labels[$index] . ' ('.$value.')'; + } + + $proportion = $value / $totalValue; + $endAngle = $startAngle + ($proportion * 360); + + $midAngle = $startAngle + ($endAngle - $startAngle) / 2; + + $labelX = $this->width / 2 + cos(deg2rad($midAngle)) * ($this->innerRadius + ($this->width / 4)) * 0.8; + $labelY = $this->height / 2 + sin(deg2rad($midAngle)) * ($this->innerRadius + ($this->height / 4)) * 0.8; + + $this->svg .= ''.$label.''; + + $startAngle = $endAngle; + } + + return $this; + } +} diff --git a/lib/svg-chart-builder/src/Svg/HorizontalBarChartBuilder.php b/lib/svg-chart-builder/src/Svg/HorizontalBarChartBuilder.php new file mode 100644 index 0000000..78ff84b --- /dev/null +++ b/lib/svg-chart-builder/src/Svg/HorizontalBarChartBuilder.php @@ -0,0 +1,177 @@ +height = max($this->height, 100 + (count($this->data) * 30)); + + $this->openSvgTag() + ->drawGraphData() + ->drawSeries() + ->drawLabels() + ->drawAxis() + ->closeSvgTag(); + + return $this->svg; + } + + /** + * Generate the X and Y axes of the bar chart. + * + * @return $this + */ + private function drawAxis() + { + $this->svg .= ''; + $this->svg .= ''; + + return $this; + } + + + /** + * Generate the chart on SVG canvas. + * + * @return $this + */ + protected function drawGraphData() + { + $numData = count($this->data); + $availableVerticalSpace = $this->height - 40; + + $totalSpace = $availableVerticalSpace - ($numData * 10); + $heightRatio = $totalSpace / $numData; + + $baseX = 100; + $baseY = $this->height - 20; + + $maxValue = max($this->data); + $maxValue = (($maxValue != 0) ? $maxValue : 1); + + $counter = 0; + $y = $baseY - $heightRatio; + + foreach ($this->data as $key => $data) { + if ($counter >= count($this->colors)) { + $counter = 0; + } + + $proportion = $data / $maxValue; + $barHeight = $proportion * ($this->width - 120); + $x = $baseX; + + $this->svg .= ''; + + $textX = $x + $barHeight / 2; + $textY = $y + $heightRatio / 2; + $this->svg .= ''.$data.''; + + $y -= $heightRatio + 10; + $counter++; + } + + return $this; + } + + /** + * Generate the labels below the Y axis of the bar chart. + * + * @return $this + */ + protected function drawLabels() + { + $numData = count($this->data); + $availableVerticalSpace = $this->height - 40; + + $totalSpace = $availableVerticalSpace - ($numData * 10); + $heightRatio = $totalSpace / $numData; + + $baseX = 95; + $baseY = $this->height - 20; + + $x = $baseX; + $y = $baseY - $heightRatio; + + foreach ($this->labels as $key => $label) { + $textY = $y + $heightRatio / 2; + + $this->svg .= ''.$label.''; + + $y -= $heightRatio + 10; + } + + return $this; + } + + /** + * Generate the X axis labels. + * + * @return $this + */ + protected function drawSeries() + { + $numTicks = $this->calculateNumTicks(); + $maxValue = max($this->data); + $maxValue = (($maxValue != 0) ? $maxValue : 1); + $interval = $maxValue / ($numTicks - 1); + + $baseX = 100; + $baseY = $this->height - 20; + + $y = $baseY + 20; + + for ($i = 0; $i < $numTicks; $i++) { + $tickValue = $i * $interval; + + if ($tickValue != (int) $tickValue) { + $tickValue = round($tickValue, 2); + } + + $barWidth = ($tickValue / $maxValue) * ($this->width - 120); + $x = $baseX + $barWidth; + + $this->svg .= ''; + + $labelX = $x; + $this->svg .= ''.$tickValue.''; + } + + return $this; + } + + protected function calculateNumTicks() + { + $maxValue = max($this->data); + $minValue = 0; + $range = $maxValue - $minValue; + $minInterval = 1; + + $numTicks = max(2, ceil($range / $minInterval) + 1); + $numTicks = min($numTicks, 11); + + return $numTicks; + } + + protected function getAxisColor($axis) + { + if (empty($this->axisColors) === false && array_key_exists($axis, $this->axisColors)) { + return $this->axisColors[$axis]; + } + + return '#000000'; + } + +} diff --git a/lib/svg-chart-builder/src/Svg/LineChartBuilder.php b/lib/svg-chart-builder/src/Svg/LineChartBuilder.php new file mode 100644 index 0000000..7d02801 --- /dev/null +++ b/lib/svg-chart-builder/src/Svg/LineChartBuilder.php @@ -0,0 +1,243 @@ +data)[0]); + $this->width = max($this->width, 100 + ($countData * 60)); + + $this->openSvgTag() + ->drawAxis() + ->drawSeries() + ->drawGraphData() + ->drawLabels() + ->drawCirclesWithText() + ->closeSvgTag(); + + return $this->svg; + } + + /** + * Generate the X and Y axes of the bar chart. + * + * @return $this + */ + private function drawAxis() + { + $this->svg .= ''; + $this->svg .= ''; + + return $this; + } + + + /** + * Generate the chart on SVG canvas. + * + * @return $this + */ + protected function drawGraphData() + { + $maxValue = $this->getMaxValue(); + $availableWidth = $this->width - 100; + $countData = (count(array_values($this->data)[0])); + $divisor = ($countData > 1) ? ($countData - 1) : $countData; + $widthRatio = $availableWidth / $divisor; + + $baseX = 50; + $baseY = 250; + + $counter = 0; + $pointAndText = ''; + + foreach ($this->data as $key => $datas) { + $x = $baseX; + $previousY = null; + + if ($counter >= count($this->colors)) { + $counter = 0; + } + + foreach ($datas as $index => $data) { + $isFirst = ($index === array_key_first($datas)); + + $proportion = $data / $maxValue; + $y = $baseY - $proportion * ($baseY - 50); + + if ($previousY !== null) { + $this->svg .= ''; + } + + $previousX = $x; + $previousY = $y; + + $pointColor = ((array_key_exists($key, $this->pointsColor) && array_key_exists($index, $this->pointsColor[$key])) ? $this->pointsColor[$key][$index] : $this->colors[$counter]); + + $pointAndText .= ''; + + $text = ((array_key_exists($key, $this->dataText) && array_key_exists($index, $this->dataText[$key])) ? $this->dataText[$key][$index] : $data); + + $pointAndText .= '' . $text . ''; + + $x += $widthRatio; + } + + $counter++; + } + + $this->svg .= $pointAndText; + + return $this; + } + + /** + * Get Max Value + * + * @return int $maxValue + */ + protected function getMaxValue() { + $maxValue = 0; + + foreach ($this->data as $labels) { + foreach ($labels as $data) { + if ($maxValue === null || $data > $maxValue) { + $maxValue = $data; + } + } + } + + return $maxValue; + } + + /** + * Generate the labels below the X axis of the chart. + * + * @return $this + */ + protected function drawLabels() + { + $availableWidth = $this->width - 100; + + $baseX = 32; + $baseY = 260; + + $rotation = -45; + $verticalOffset = (10 * abs(sin(deg2rad($rotation)))) + 30; + + foreach ($this->labels as $index => $label) { + $countLabels = count($this->labels); + $divisor = ($countLabels > 1) ? ($countLabels - 1) : $countLabels; + $proportion = $index / $divisor; + $labelX = $baseX + $proportion * $availableWidth; + + $this->svg .= ''.$label.''; + } + + return $this; + } + + + protected function drawCirclesWithText() + { + if($this->bannerInfo) { + $labels = array_keys($this->data); + $availableWidth = $this->width - 100; + + $baseX = 50; + $baseY = 250; + + foreach ($labels as $index => $label) { + if (is_string($label)) { + $proportion = $index / (count($labels) - 1); + $x = $baseX + $proportion * $availableWidth; + + $circleY = $baseY + 90; + $circleColor = $this->colors[$index]; + + $this->svg .= ''; + $this->svg .= '' . $label . ''; + } + } + } + + return $this; + } + + /** + * Generate the Y axis labels. + * + * @return $this + */ + protected function drawSeries() + { + $numTicks = $this->calculateNumTicks(); + $maxValue = $this->getMaxValue(); + $interval = $maxValue / ($numTicks - 1); + + $baseX = 40; + $baseY = 250; + + $x = $baseX - 10; + + for ($i = 0; $i < $numTicks; $i++) { + $tickValue = $i * $interval; + + if ($tickValue != (int) $tickValue) { + $tickValue = round($tickValue, 2); + } + + $barHeight = ($tickValue / $maxValue) * ($baseY - 50); + $y = $baseY - $barHeight; + + $this->svg .= ''; + + $labelY = $y + 5; + $this->svg .= ''.$tickValue.''; + } + + return $this; + } + + protected function calculateNumTicks() + { + $maxValue = $this->getMaxValue(); + $minValue = 0; + $range = $maxValue - $minValue; + $minInterval = 1; + + $numTicks = max(2, ceil($range / $minInterval) + 1); + $numTicks = min($numTicks, 11); + + return $numTicks; + } + + protected function getAxisColor($axis) + { + if (empty($this->axisColors) === false && array_key_exists($axis, $this->axisColors)) { + return $this->axisColors[$axis]; + } + + return '#000000'; + } + +} diff --git a/lib/svg-chart-builder/src/Svg/PieChartBuilder.php b/lib/svg-chart-builder/src/Svg/PieChartBuilder.php new file mode 100644 index 0000000..ec77cf1 --- /dev/null +++ b/lib/svg-chart-builder/src/Svg/PieChartBuilder.php @@ -0,0 +1,103 @@ +openSvgTag() + ->drawGraphData() + ->drawLabels() + ->closeSvgTag(); + + return $this->svg; + } + + /** + * Draw the slices of the pie chart based on the data. + * + * @return $this + */ + protected function drawGraphData() + { + $totalValue = array_sum($this->data); + $startAngle = 0; + $counter = 0; + + foreach ($this->data as $value) { + if ($value <= 0) { + continue; + } + + if ($counter >= count($this->colors)) { + $counter = 0; + } + + $proportion = $value / $totalValue; + $endAngle = $startAngle + ($proportion * 360); + + $startX = $this->width / 2; + $startY = $this->height / 2; + + $endX1 = $startX + cos(deg2rad($startAngle)) * ($this->width / 2); + $endY1 = $startY + sin(deg2rad($startAngle)) * ($this->height / 2); + + $endX2 = $startX + cos(deg2rad($endAngle)) * ($this->width / 2); + $endY2 = $startY + sin(deg2rad($endAngle)) * ($this->height / 2); + + $largeArcFlag = $proportion > 0.5 ? 1 : 0; + + $this->svg .= ''; + + $startAngle = $endAngle; + $counter++; + } + + return $this; + } + + /** + * Draw the labels for each slice on the chart. + * + * @return $this + */ + protected function drawLabels() + { + $totalValue = array_sum($this->data); + $startAngle = 0; + + foreach ($this->data as $index => $value) { + if ($value <= 0) { + continue; + } + + $label = $value; + if (array_key_exists($index, $this->labels)) { + $label = $this->labels[$index] . ' ('.$value.')'; + } + + $proportion = $value / $totalValue; + $endAngle = $startAngle + ($proportion * 360); + + $midAngle = $startAngle + ($endAngle - $startAngle) / 2; + + $labelX = $this->width / 2 + cos(deg2rad($midAngle)) * ($this->width / 4); + $labelY = $this->height / 2 + sin(deg2rad($midAngle)) * ($this->height / 4); + + $this->svg .= ''.$label.''; + + $startAngle = $endAngle; + } + + return $this; + } +} diff --git a/lib/svg-chart-builder/tests/.gitkeep b/lib/svg-chart-builder/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/svg-chart-builder/tests/BarChartBuilderTest.php b/lib/svg-chart-builder/tests/BarChartBuilderTest.php new file mode 100644 index 0000000..70b31ca --- /dev/null +++ b/lib/svg-chart-builder/tests/BarChartBuilderTest.php @@ -0,0 +1,24 @@ + 16, + 'literatura' => 18, + 'inglés' => 40, + // ... más datos de prueba ... + ]; + + $chart = (new SVGChartBuilder('bar', $data))->create(); + + $this->assertNotEmpty($chart); + } +} diff --git a/lib/svg-chart-builder/tests/SVGChartBuilderTest.php b/lib/svg-chart-builder/tests/SVGChartBuilderTest.php new file mode 100644 index 0000000..2f1dc39 --- /dev/null +++ b/lib/svg-chart-builder/tests/SVGChartBuilderTest.php @@ -0,0 +1,30 @@ + 16, + 'literatura' => 18, + 'inglés' => 40, + ]; + $chart = (new SVGChartBuilder('bar', $data))->create(); + + $this->assertNotEmpty($chart); + + $this->assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringContainsString('> GLFTPD-WEBUI-LOCAL-INSTALLER ################################## ################################ #### # ## @@ -237,7 +237,7 @@ func_ports() { } } { - print g,x":"hextodec(substr($2,index($2,":")+1,4)) + print g,x":"hextodec(substr($2,index($2,":")+1,4)) }' < /proc/net/tcp } diff --git a/src/config.php.dist b/src/config.php.dist index 3d829b5..b9f9b5b 100644 --- a/src/config.php.dist +++ b/src/config.php.dist @@ -4,20 +4,36 @@ * GLFTPD:WEBUI configuration *---------------------------------------------------------------------------*/ +// for theme button colors see: +// - https://getbootstrap.com/docs/4.0/components/buttons/ +// - plus these extra colors: 'custom', 'purple', 'gray' + return $cfg = array( - 'auth' => "basic", - 'mode' => "docker", - 'show_more_opts' => false, - 'show_alerts' => true, - 'max_items' => 10, - 'debug' => 0, - 'http_auth' => ['username' => 'shit', 'password' => 'EatSh1t'], - 'spy' => ['enabled' => true, 'refresh' => true], - 'modal' => ["commands" => false, "sitewho" => true], - 'title' => ' - GLFTPD: - - COMMAND CENTER', + 'auth' => "basic", + 'mode' => "docker", + 'show_alerts' => true, + 'show_more_options' => true, + 'auto_scroll' => false, + 'max_items' => 10, + 'debug' => 0, + 'http_auth' => ['username' => 'shit', 'password' => 'EatSh1t'], + 'glftpd_auth' => ['check_siteop' => true, 'check_ip_mask' => true ], + 'spy' => ['show' => true, 'refresh' => true], + 'modal' => array( + 'commands' => false, + 'sitewho' => true, + 'userstats' => false, + 'all_stats' => true, + ), + 'theme' => array( + 'title' => ' + GLFTPD: + + COMMAND CENTER', + 'btn-color-1' => 'primary', + 'btn-color-2' => 'secondary', + 'btn-small-color' => 'gray', + ), 'services' => array( "glftpd" => ['host' => "localhost", 'port' => "1337"], @@ -74,4 +90,61 @@ return $cfg = array( 'close tty' => ['cmd' => 'kill_gotty', 'sep' => true ], ), ), + + 'stats' => array( + 'commands' => array( + 'DAYUP' => ['cmd' => 'usertop_raw', 'stat' => 'DAYUP', 'show' => 1], + 'WKUP' => ['cmd' => 'usertop_raw', 'stat' => 'WKUP', 'show' => 1], + 'MONTHUP' => ['cmd' => 'usertop_raw', 'stat' => 'MONTHUP', 'show' => 2], + 'ALLUP' => ['cmd' => 'usertop_raw', 'stat' => 'ALLUP', 'show' => 1], + 'DAYDN' => ['cmd' => 'usertop_raw', 'stat' => 'DAYDN', 'show' => 0], + 'WKDN' => ['cmd' => 'usertop_raw', 'stat' => 'WKDN', 'show' => 0], + 'MONTHDN' => ['cmd' => 'usertop_raw', 'stat' => 'MONTHDN', 'show' => 0], + 'ALLDN' => ['cmd' => 'usertop_raw', 'stat' => 'ALLDN', 'show' => 0], + 'NUKE' => ['cmd' => 'usertop_raw', 'stat' => 'NUKE', 'show' => 0], + 'GDAYUP' => ['cmd' => 'grouptop_raw', 'stat' => 'DAYUP', 'show' => 1], + 'GWKUP' => ['cmd' => 'grouptop_raw', 'stat' => 'WKUP', 'show' => 1], + 'GMONTHUP' => ['cmd' => 'grouptop_raw', 'stat' => 'MONTHUP', 'show' => 1], + 'GALLUP' => ['cmd' => 'grouptop_raw', 'stat' => 'ALLUP', 'show' => 1], + 'GDAYDN' => ['cmd' => 'grouptop_raw', 'stat' => 'DAYDN', 'show' => 0], + 'GWKDN' => ['cmd' => 'grouptop_raw', 'stat' => 'WKDN', 'show' => 0], + 'GMONTHDN' => ['cmd' => 'grouptop_raw', 'stat' => 'MONTHDN', 'show' => 0], + 'GALLDN' => ['cmd' => 'grouptop_raw', 'stat' => 'ALLDN', 'show' => 0], + ), + 'options' => array( + 'max_pos' => 20, + 'color' => 'darkgreen', + ), + ), + + 'palette' => array( + 'default' => array( + '#a9d6e5', + '#89c2d9', + '#61a5c2', + '#468faf', + '#2c7da0', + '#2a6f97', + ), + 'lightblue' => array( + '#caf0f8', + '#ade8f4', + '#90e0ef', + '#48cae4', + '#00b4d8', + '#0096c7', + '#0077b6', + '#023e8a', + ), + 'darkgreen' => array( + '#dde7c7', + '#c2c5aa', + '#a4ac86', + '#6d8257', + '#656d4a', + '#b6ad90', + '#dad7cd', + ), + ), + ); diff --git a/src/ui/controller.php b/src/ui/controller.php index 133798f..9dded69 100644 --- a/src/ui/controller.php +++ b/src/ui/controller.php @@ -51,9 +51,9 @@ function set_cmd_result($cmd_out) { // form submits, input controls and routing. mark changed values if (isset($_SESSION['postdata'])) { - //if (cfg::get('debug') > 0) { - // $debug->print(pos: 'controller-1 set_cmd_result', _SESSION_postdata: $_SESSION['postdata']); - //} + if (cfg::get('debug') > 10) { + $debug->print(pos: 'controller-1 set_cmd_result', _SESSION_postdata: $_SESSION['postdata']); + } // 'xxCmd' buttons for logs etc if (!empty($_SESSION['postdata']['dockerCmd'])) { if ($_SESSION['postdata']['dockerCmd'] === 'docker_logs_glftpd') { @@ -69,7 +69,7 @@ function set_cmd_result($cmd_out) { if ($_SESSION['postdata']['dockerCmd'] === 'docker_inspect_glftpd') { include_once 'templates/logs.html'; print(PHP_EOL . 'Output from docker inpect glftpd...' . PHP_EOL . PHP_EOL); - print format_cmdout($data->func($_SESSION['postdata']['dockerCmd'])); + print format_cmd_out($data->func($_SESSION['postdata']['dockerCmd'])); unset($_SESSION['postdata']['dockerCmd']); print '' . PHP_EOL . '' . PHP_EOL . '' . PHP_EOL; exit; @@ -78,18 +78,24 @@ function set_cmd_result($cmd_out) { if (!empty($_SESSION['postdata']['gltoolCmd'])) { if ($_SESSION['postdata']['gltoolCmd'] === 'gltool_log') { include_once 'templates/logs.html'; - print format_cmdout($data->func($_SESSION['postdata']['gltoolCmd'])); + print format_cmd_out($data->func($_SESSION['postdata']['gltoolCmd'])); unset($_SESSION['postdata']['gltoolCmd']); print '' . PHP_EOL . '' . PHP_EOL . '' . PHP_EOL; exit; } - if ($_SESSION['postdata']['gltoolCmd'] === "show_user_stats") { + if ($_SESSION['postdata']['gltoolCmd'] === "show_userstats") { if ($data->check_user() && isset($_SESSION['userfile'])) { - $html = htmlspecialchars(addslashes(format_user_stats())); + $text = format_userstats(); + $html = htmlspecialchars(addslashes($text)); } else { + $text = ""; $html = "<user:none>"; } - $_SESSION['modal'] = array('func' => 'show', 'title' => "User Stats", 'text' => $html); + if (cfg::get('modal')['userstats']) { + $_SESSION['modal'] = array('func' => 'show', 'title' => "User Stats", 'text' => $html); + } else { + $_SESSION['cmd_output'] = $text; + } unset($_SESSION['postdata']['gltoolCmd']); } } @@ -105,6 +111,35 @@ function set_cmd_result($cmd_out) { exit; } + if (isset($_SESSION['postdata']['show_all_stats'])) { + if (cfg::get('modal')['all_stats'] && !isset($_SESSION['postdata']['stats_page'])) { + $out = '

'; + foreach (cfg::get('stats')['commands'] as $key => $item) { + if ($item['show'] >= 1) { + $result = $data->get_chart_stats($item); + $color = cfg::get('stats')['options']['color']; + $svg = create_svg("pie", $result['chart_data'], $result['chart_labels'], cfg::get('palette')[$color]); + $out .= "

{$item['stat']} " . ((substr($key, 0, 1) === 'G' ? "GROUP" : "USER") . "
"); + $out .= "
"; + $out .= str_replace(["\r\n", "\r", "\n", "\t"], ' ', $svg); + $out .= "
"; + $out .= "
"; + $out .= format_stats($result); + $out .= "
"; + } + } + $_SESSION['modal'] = array('func' => 'show', 'title' => "All Stats", 'text' => htmlspecialchars(addslashes("
{$out}
"))); + unset($_SESSION['postdata']['show_all_stats']); + } else { + include_once 'templates/stats.html.php'; + if ($_SESSION['postdata']['stats_page']) { + unset($_SESSION['postdata']['stats_page']); + } + unset($_SESSION['postdata']['show_all_stats']); + exit; + }; + } + // loop over postdata to get remaining inputs foreach ($_SESSION['postdata'] as $name => $value) { @@ -149,7 +184,7 @@ function set_cmd_result($cmd_out) { unset($_SESSION['postdata'][$name]); } elseif ($name === "termCmd" && !empty($value)) { if (preg_match('/^kill_[a-z_]+$/', $value)) { - $_SESSION['cmd_output'] = format_cmdout($data->func($value)); + $_SESSION['cmd_output'] = format_cmd_out($data->func($value)); } else { set_cmd_result($data->func($value)); $_SESSION['modal'] = array('func' => 'tty'); @@ -168,7 +203,7 @@ function set_cmd_result($cmd_out) { $data->func($value); $_SESSION['results'][$value] = "DONE: {$value}"; } else { - $_SESSION['cmd_output'] = format_cmdout($data->func($value)); + $_SESSION['cmd_output'] = format_cmd_out($data->func($value)); } } unset($_SESSION['postdata'][$name]); @@ -358,11 +393,11 @@ function set_cmd_result($cmd_out) { unset($_SESSION['postdata']['tagCmd']); } unset($_SESSION['postdata']['applyBtn']); - if ($data->check_user() && !empty($_SESSION['postdata']['userCmd']) && $_SESSION['postdata']['userCmd'] === 'reset_user_stats') { + if ($data->check_user() && !empty($_SESSION['postdata']['userCmd']) && $_SESSION['postdata']['userCmd'] === 'reset_userstats') { $replace_pairs = array( '{$username}' => $_SESSION['postdata']['select_user'], ); - set_cmd_result($data->func(['reset_user_stats', $replace_pairs])); + set_cmd_result($data->func(['reset_userstats', $replace_pairs])); unset($_SESSION['postdata']['userCmd']); } unset($_SESSION['postdata']['applyBtn']); diff --git a/src/ui/docker_api.php b/src/ui/docker_api.php index 1ca9d0d..0b58fdd 100644 --- a/src/ui/docker_api.php +++ b/src/ui/docker_api.php @@ -23,14 +23,14 @@ class docker { private array $commands; private $debug; - + public function __construct() { $this->commands = require 'docker_commands.php'; $this->debug = new debug; $this->debug->count = 0; } - public function api(string $http_method, string $endpoint, $postfields=null): string|bool { + public function api(string $http_method, string $endpoint, $postfields = null): string|bool { $url = cfg::get('docker')['api'] . $endpoint; $this->debug->trace(count: $this->debug->count++, trace: 'docker-api-1', url: $url, postfields: $postfields); $ch = curl_init(); @@ -77,6 +77,18 @@ public function api(string $http_method, string $endpoint, $postfields=null): st return $data; } + public function format_str(string $result): array { + $return = array(); + $lines = explode(PHP_EOL, trim(substr($result, 8))); + foreach ($lines as $line) { + $line = trim(sanitize_string($line)); + if (!empty($line)) { + array_push($return, $line); + } + } + return $return; + } + public function test_port(string $container, string $host, string $port): bool { $exec = json_decode( self::api( @@ -84,7 +96,7 @@ public function test_port(string $container, string $host, string $port): bool { "/containers/$container/exec", '{ "AttachStdout": true, "Tty": false, "Cmd": [ - "echo", "|", "/bin/busybox", "telnet", "'. $host . '", "' . $port . '" + "echo", "|", "/bin/busybox", "telnet", "' . $host . '", "' . $port . '" ] }' ) @@ -120,7 +132,7 @@ public function exec(string $id, string $cmd): mixed { if (!preg_match('/exec failed/i', $result)) { $this->debug->trace(trace: 'docker-exec-3', result: $result); $json_result = json_decode($result); - // exec ok, but error in cmd output + // XXX: handle exec status ok, but specfic error in cmd output /* if (preg_match('/operation not permitted/i', $result)) { return 'Error: EPERM'; @@ -132,25 +144,13 @@ public function exec(string $id, string $cmd): mixed { if (preg_match('/no such file or directory/i', $result)) { return 'Error: ENOENT'; } - return (json_last_error() === JSON_ERROR_NONE) ? $json_result : $this->format($result); + return (json_last_error() === JSON_ERROR_NONE) ? $json_result : $this->format_str($result); } } } return false; } - public function format(string $result): array { - $return = array(); - $lines = explode(PHP_EOL, trim(substr($result, 8))); - foreach($lines as $line) { - $line = trim(sanitize_string($line)); - if (!empty($line)) { - array_push($return, $line); - } - } - return $return; - } - // disabled //public function create(array $hostconfig=[]): object { public function create(): bool { @@ -179,7 +179,8 @@ public function create(): bool { } } }' - ), $hostconfig + ), + $hostconfig ) ); } @@ -188,11 +189,11 @@ public function start(string $id) { return json_decode(self::api("POST", "/containers/{$id}/start", "")); } - public function list(bool $all=false) { + public function list(bool $all = false) { return json_decode(self::api("GET", "/containers/json?all=$all")); } - public function func(array|string $args): mixed { + public function func(array|string $args): mixed { $action = is_array($args) ? $args[0] : $args; $command = $this->commands[$action]; if (isset($command)) { diff --git a/src/ui/docker_commands.php b/src/ui/docker_commands.php index dddc1c1..980d792 100644 --- a/src/ui/docker_commands.php +++ b/src/ui/docker_commands.php @@ -67,7 +67,9 @@ 'tag_raw' => array("exec", '{$gl_ct_name}', '["{$bin_dir}/gltool.sh", "-c", "RAWTAG", "-u", "{$username}"]'), 'flag_raw' => array("exec", '{$gl_ct_name}', '["{$bin_dir}/gltool.sh", "-c", "RAWFLAG", "-u", "{$username}"]'), 'creds_raw' => array("exec", '{$gl_ct_name}', '["{$bin_dir}/gltool.sh", "-c", "RAWCREDS", "-u", "{$username}"]'), - 'reset_user_stats' => array("exec", '{$gl_ct_name}', '["{$bin_dir}/gltool.sh", "-c", "RESETUSERSTATS", "-u", "{$username}"]'), + 'reset_userstats' => array("exec", '{$gl_ct_name}', '["{$bin_dir}/gltool.sh", "-c", "RESETUSERSTATS", "-u", "{$username}"]'), + 'usertop_raw' => array("exec", '{$gl_ct_name}', '["{$bin_dir}/gltool.sh", "-c", "RAWUSERTOP", "-x", "{$stat}"]'), + 'grouptop_raw' => array("exec", '{$gl_ct_name}', '["{$bin_dir}/gltool.sh", "-c", "RAWGROUPTOP", "-x", "{$stat}"]'), 'change_auth' => array("exec", '{$web_ct_name}', '["/auth.sh", "{$mode}", "{$username}", "{$password}"]'), 'nginx_reload' => array("exec", '{$web_ct_name}', '["nginx", "-s", "reload"]'), 'ps_gotty_proc' => array("exec", '{$gl_ct_name}', '["sh", "-c", "grep -ar [gG]otty /proc/[0-9]*/cmdline"]'), diff --git a/src/ui/format.php b/src/ui/format.php index 3ff4b7b..c63cd5d 100644 --- a/src/ui/format.php +++ b/src/ui/format.php @@ -4,7 +4,10 @@ * SHIT:FRAMEWORK formatting *--------------------------------------------------------------------------*/ -// ghetto json parsing and fmt'ing :) +//use shit\data; +//require_once 'get_data.php'; + +// ghetto json parsing and formatting :) function format_json($json): string { try { @@ -39,6 +42,8 @@ function format_json($json): string { } } +// XXX: unused +/* function format_msg_logs($output): string { $result = ""; foreach ($output as $line) { @@ -46,6 +51,7 @@ function format_msg_logs($output): string { } return $result; } +*/ function format_procs($json): string { $result = "
Processes:
" . PHP_EOL; @@ -59,7 +65,7 @@ function format_procs($json): string { // docker mode: format json result // local mode: format $output from exec($command, $output, $result_code); -function format_cmdout(mixed $result): mixed { +function format_cmd_out(mixed $result): mixed { $out = null; if (is_array($result)) { $out = implode(PHP_EOL, $result); @@ -88,37 +94,42 @@ function format_cmdout(mixed $result): mixed { function format_bytes(int $size, int $precision = 2): string { if ($size > 0) { $base = log($size, 1024); - $suffixes = array('', 'K', 'M', 'G', 'T'); + $suffixes = array('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi'); return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)] . "B"; } - return "0KB"; + return "0b"; } -function format_lastlogin(): string { +function format_login(): string { if (!empty($_SESSION['userfile']) && !empty($_SESSION['userfile']['TIME'])) { - $epoch = explode(' ', $_SESSION['userfile']['TIME'])[1]; - $dt = new DateTime("@$epoch"); - return $dt->format('Y-m-d H:i'); + $values = explode(' ', $_SESSION['userfile']['TIME']); + print('
DEBUG
'); + print_r($values, true); + if ($values[0] > 0 && $values[1] > 0) { + $dt = new DateTime("@$values[1]"); + return $dt->format('Y-m-d H:i'); + } } + return false; } -function format_stats(string $field): array { +function format_stat_section(string $field): array { $i=0; - $s=0; + $snum=0; $section = array(); if (isset($_SESSION['userfile'][$field])) { - foreach(explode(' ', $_SESSION['userfile'][$field]) as $v) { + foreach(explode(' ', $_SESSION['userfile'][$field]) as $value) { switch ($i) { case $i > 2: $i=0; case 0: - $section[$s][0] = $v; + $section[$snum][0] = $value; case 1: - $section[$s][1] = $v; + $section[$snum][1] = $value; break; case 2: - $section[$s][2] = $v; - $s++; + $section[$snum][2] = $value; + $snum++; //break; default: } @@ -128,20 +139,17 @@ function format_stats(string $field): array { return $section; } -function format_user_stats(): string { - $all_fields = array('DAYUP', 'WKUP ', 'MONTHUP', 'ALLUP', '', 'DAYDN', 'WKDN', 'MONTHDN', 'ALLDN', '', 'NUKE', ''); - $out = "

"; - $out .= "Showing stats for {$_SESSION['postdata']['select_user']}
"; - if (!empty($_SESSION['userfile']) && !empty($_SESSION['userfile']['TIME'])) { - $out .= "LAST LOGIN: " . format_lastlogin(); - } else { - $out .= "<none>"; - } - $out .= "

"; - $out .= sprintf("PERIOD UP/DN%-4s[STAT_SECTION]%-5sFiles / Bytes", "", "") . "
"; - $out .= sprintf("%'-*s", 80, "-") . "
"; - foreach($all_fields as $field) { - $stats = format_stats($field); +function format_userstats(): string { + $all_userstats = array('DAYUP', 'WKUP ', 'MONTHUP', 'ALLUP', '', 'DAYDN', 'WKDN', 'MONTHDN', 'ALLDN', '', 'NUKE', ''); + $out = "

" . + "Showing stats for {$_SESSION['postdata']['select_user']}
" . + "LAST LOGIN: " . ((format_login()) ? format_login() : "<none>") . + "

" . + sprintf("STATS UP/DN%-4s[STAT_SECTION]%-5sFiles / Bytes", "", "") . "
" . + sprintf("%'-*s", 80, "-") . "
"; + + foreach($all_userstats as $field) { + $stats = format_stat_section($field); if (!empty($_SESSION['userfile'][$field])) { $out .= sprintf("%-11s", $field); for ($i = 0; $i < count($stats); $i++) { @@ -149,7 +157,7 @@ function format_user_stats(): string { $out .= ($i === 0) ? sprintf("%17s%7s", "[{$i}](DEFAULT)", "") : sprintf("%19s%16s", "[{$i}]", ""); if ($field === "NUKE") { $last = ""; - $epoch = $stats[$i][0]; + $epoch = $stats[$i][2]; if (!empty($epoch) && $epoch > 0) { $dt = new DateTime("@$epoch"); $last = $dt->format('y-m-d H:i'); @@ -165,3 +173,20 @@ function format_user_stats(): string { $out .= "
"; return $out; } + +function format_stats($item) { + $pos = 1; + $out = ""; + foreach ($item['fields_all'] as $fields) { + if ($pos <= cfg::get('stats')['options']['max_pos']) { + $out .= "
"; + $out .= sprintf("%02d. ", $pos) . (($pos === 1) ? "{$fields[0]}" : $fields[0]) . " {$fields[1]} ({$fields[2]})"; + $out .= "
"; + } + $pos++; + } + for ($pos; $pos <= cfg::get('stats')['options']['max_pos']; $pos++) { + $out .= "
 
"; + } + return $out; +} diff --git a/src/ui/get_data.php b/src/ui/get_data.php index 061994e..50ac672 100644 --- a/src/ui/get_data.php +++ b/src/ui/get_data.php @@ -6,7 +6,6 @@ // TODO: use reflection instead of call_user_func_array? // https://www.php.net/manual/en/reflectionfunction.invokeargs.php -// cleanup namespace shit; @@ -40,6 +39,7 @@ public function func($args): mixed { $this->debug->print(pos: 'get_data func', args: $args); } + //TODO: cleanup /* if (cfg::get('debug') > 9) { $this->cfg = $args; @@ -267,4 +267,27 @@ public function get_status() { } //$_SESSION['update']['status'] = true; } + + public function get_chart_stats($item): array { + $fields_all = array (); + $chart_data = array(); + $chart_labels = array(); + foreach ($this->func([$item['cmd'], array('{$stat}' => $item['stat'])]) as $line) { + $fields = explode(' ', trim(sanitize_string($line))); + $format_fields = array( + $fields[0], + format_bytes((int)$fields[1], 0), + (!empty($fields[2]) ? $fields[2] : 0) . "%" + ); + array_push($fields_all, $format_fields); + array_push($chart_labels, $fields[0]); + array_push($chart_data, (!empty($fields[2]) ? $fields[2] : 0)); + } + return array( + 'fields_all' => $fields_all, + 'chart_labels' => $chart_labels, + 'chart_data' => $chart_data, + ); + } + } diff --git a/src/ui/graphs.php b/src/ui/graphs.php new file mode 100644 index 0000000..d8581af --- /dev/null +++ b/src/ui/graphs.php @@ -0,0 +1,91 @@ + [ + 'math', + 'literature', + 'english', + // ... other data ... + ], + 'colors' => [ + '#CDDC39', + '#00BCD4', + '#9E9E9E', + // ... other data ... + ], + 'axisColors' => [ + 'x' => 'red', + 'y' => 'blue' + ], + 'labelsColor' => 'orange', + 'dataColor' => 'white', +]; +*/ + +function create_svg ($type, $data = [], $labels = [], $palette) { + $check_data=0; + for ($i = 1; $i < count($data); $i++) { + $check_data += $data[$i]; + }; + if ($check_data > 0) { + if (empty($palette)) { + $palette = [ + '#CDDC39', + '#00BCD4', + '#9E9E9E', + ]; + } + $n = 0; + for ($i = 0; $i < count($data); $i++) { + if ($i % count($palette) == 0) { + $n = 0; + } + $colors[$i] = $palette[$n]; + $n++; + }; + if (!empty($type) && !empty($data) && !empty($labels) && !empty($colors)) { + $options = [ + 'labels' => $labels, + 'labelsColor' => '#333', + 'dataColor' => '#111', + 'height' => 280, + 'width' => 280, + 'colors' => $colors, + ]; + $chartBuilder = new SVGChartBuilder($type, $data, $options); + return $chartBuilder->create(); + } + } else { + return(' + + ' . $labels[0] . ' (' . $data[0] . ')' . ' + <no data> + + + '); + } + return false; +} diff --git a/src/ui/index.php b/src/ui/index.php index 87c44b8..77970f3 100644 --- a/src/ui/index.php +++ b/src/ui/index.php @@ -1,6 +1,6 @@ print(ppos: 'index', get: $_GET['user'], select_user: $_SESSION['postdata']['select_user']); - if (cfg::get('debug') > 0) { unset($_SESSION['DEBUG']); $_SESSION['DEBUG'] = array(); @@ -314,9 +318,6 @@ $debug->print(pre: true, pos: 'index', _SESSION_postdata: $_SESSION['postdata']); } -//$debug->print(pre: true, pos: 'index', _SESSION_results: $_SESSION['results'], , _SESSION_cmd_output: $_SESSION['cmd_output']); -//$debug->print(pre: true, pos: 'index', _SESSION: $_SESSION); - /*--------------------------------------------------------------------------*/ /* DATA @@ -389,8 +390,6 @@ unset($_SESSION['results']); -// $debug->print(pre: true, pos: 'index [2]', _SESSION_cmd_output: $_SESSION['cmd_output']); - /*--------------------------------------------------------------------------*/ /* TEMPLATE @@ -417,7 +416,7 @@ print_r($_SESSION['DEBUG'], true) . "
" . PHP_EOL; } -if (cfg::get('spy')['enabled']) { +if (cfg::get('spy')['show']) { print '' . PHP_EOL; if (!cfg::get('spy')['refresh']) { print '' . PHP_EOL; diff --git a/src/ui/local_commands.php b/src/ui/local_commands.php index 1fb57a5..0a4aa2a 100644 --- a/src/ui/local_commands.php +++ b/src/ui/local_commands.php @@ -66,7 +66,9 @@ 'tag_raw' => '"{$bin_dir}/gltool.sh" -c RAWTAG -u "{$username}"', 'flag_raw' => '"{$bin_dir}/gltool.sh" -c RAWFLAG -u "{$username}"', 'creds_raw' => '"{$bin_dir}/gltool.sh" -c RAWCREDS -u "{$username}"', - 'reset_user_stats' => '"{$bin_dir}/gltool.sh" -c RESETUSERSTATS -u "{$username}"', + 'reset_userstats' => '"{$bin_dir}/gltool.sh" -c RESETUSERSTATS -u "{$username}"', + 'usertop_raw' => '"{$bin_dir}/gltool.sh" -c RAWUSERTOP -x "{$stats}"', + 'grouptop_raw' => '"{$bin_dir}/gltool.sh" -c RAWGROUPTOP -x "{$stats}"', 'change_auth' => '{$runas} "{$bin_dir}/auth.sh {$mode} {$username} {$password}', 'nginx_reload' => '{$runas} nginx -s reload', 'glftpd_conn' => '{$runas} netstat -nap|grep {$this->cfg["glftpd"]["port"} || {$runas} ss -nap|grep {$this->cfg["glftpd"]["port"}; }' diff --git a/src/ui/show.php b/src/ui/show.php index 3c3006d..c28126c 100644 --- a/src/ui/show.php +++ b/src/ui/show.php @@ -60,7 +60,7 @@ function show_notifications(...$args) { // show cmd results from controller if (!empty(($_SESSION['results']))) { foreach(($_SESSION['results']) as $result) { - print " " . PHP_EOL; + print "
' role='alert'>{$result}
" . PHP_EOL; print '

' . PHP_EOL; } } else { @@ -80,7 +80,7 @@ function show_notifications(...$args) { print "
" . PHP_EOL; } if (isset($args['reload']) && $args['reload'] === "button") { - print ' ' . PHP_EOL; + print ' ' . PHP_EOL; } print '

' . PHP_EOL; print '
' . PHP_EOL; @@ -101,7 +101,9 @@ function show_output() { print '
' . PHP_EOL;
     print $_SESSION['cmd_output'];
     print "
" . PHP_EOL; - print JS_SCROLL; + if (cfg::get('auto_scroll')) { + print JS_SCROLL; + } unset($_SESSION['cmd_output']); } diff --git a/templates/about.html b/templates/about.html index af4b537..2cbe300 100644 --- a/templates/about.html +++ b/templates/about.html @@ -34,15 +34,11 @@ and can be used to view logs, edit config files and browse site. Also has a browser terminal that displays tools like gl_spy, useredit and bot partyline (using websockets). - Additional tools and libs used: + Additional tools and libs used: see README.md - Tiny File Manager offline mode (filemanager.github.io) - GoTTY web terminal (github.com/sorenisanerd/gotty) - gltool.sh bash (github.com/silv3rr/scripts) - pyspy flask mode (github.com/silv3rr/pyspy) - pywho (github.com/silv3rr/pywho) + [G] Goodbye ..NO CARRIER \ No newline at end of file diff --git a/templates/change.html.php b/templates/change.html.php index 9d8f88c..0403761 100644 --- a/templates/change.html.php +++ b/templates/change.html.php @@ -34,7 +34,7 @@
- @@ -55,7 +55,7 @@
- +

@@ -71,7 +71,7 @@ 0): ?>
$gadmin): ?> - + @@ -83,7 +83,7 @@ get_user_pgroup(); if (!empty($user_pgroups)): ?> - + * @@ -74,15 +74,15 @@
check_user()): ?>
- +
- - + Reset - +
@@ -110,15 +110,15 @@
- -
diff --git a/templates/groups.html.php b/templates/groups.html.php index a0b9879..860e1a0 100644 --- a/templates/groups.html.php +++ b/templates/groups.html.php @@ -7,18 +7,22 @@ @@ -26,7 +30,9 @@
$desc): ?> - " class="mr-2"> + " class="mr-2"> + + cfg::get('max_items')): ?> @@ -46,7 +52,9 @@
- +
diff --git a/templates/logs.html b/templates/logs.html index 3ff9544..ee7ff13 100644 --- a/templates/logs.html +++ b/templates/logs.html @@ -1,6 +1,6 @@ - SHIT:TXT:VIEWER + SHIT:TXTVIEWER diff --git a/templates/main.html.php b/templates/main.html.php index 9fbe55f..8803294 100644 --- a/templates/main.html.php +++ b/templates/main.html.php @@ -8,8 +8,11 @@ - - + + + + + @@ -18,9 +21,7 @@ - -
-
+
  MODE: @@ -40,20 +41,23 @@
DARK THEME
-
- - - - + + +
+ - +
@@ -85,8 +89,8 @@

- -
+ +

-   -
@@ -124,7 +129,7 @@
0 || $fm_data['count']['files']['all'] > 0): ?> -
+
+ + + +
+ $item): ?> + = 1): ?> +
+
TOP
+

+
+ + get_chart_stats($item); ?> + + + +
+ . {$fields[0]}" : $fields[0] ?> () +
+ + + + +
 
+ + + +
+
+ + +
+ + + + \ No newline at end of file diff --git a/templates/users.html.php b/templates/users.html.php index 6781584..775b01c 100644 --- a/templates/users.html.php +++ b/templates/users.html.php @@ -6,23 +6,29 @@
- +
- - + +
- +
@@ -35,7 +41,9 @@ $group): ?>
- +
@@ -74,11 +82,14 @@
- + +
-