From: Paul Hänsch Date: Thu, 7 Oct 2021 22:32:28 +0000 (+0200) Subject: Merge commit '178c015a44368ed1aa2e400ddc5f52c84944b196' X-Git-Url: https://git.plutz.net/?a=commitdiff_plain;h=3be70af0db638f7a7a98ec80faf3923a11015a21;hp=178c015a44368ed1aa2e400ddc5f52c84944b196;p=rawnet Merge commit '178c015a44368ed1aa2e400ddc5f52c84944b196' --- diff --git a/.gitignore b/.gitignore index 5c9950a..99d7f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -cgilite -serverkey +channels.db users.db +serverkey +[0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=]/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24781a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: _subtrees + +_subtrees: _cgilite + +cgilite: + git subtree add --squash -P $@ https://git.plutz.net/git/$@ master + +_cgilite: cgilite + git subtree pull --squash -P $< https://git.plutz.net/git/$< master diff --git a/cgilite/.gitignore b/cgilite/.gitignore new file mode 100644 index 0000000..5c9950a --- /dev/null +++ b/cgilite/.gitignore @@ -0,0 +1,3 @@ +cgilite +serverkey +users.db diff --git a/cgilite.sh b/cgilite/cgilite.sh similarity index 100% rename from cgilite.sh rename to cgilite/cgilite.sh diff --git a/common.css b/cgilite/common.css similarity index 100% rename from common.css rename to cgilite/common.css diff --git a/file.sh b/cgilite/file.sh similarity index 100% rename from file.sh rename to cgilite/file.sh diff --git a/html-sh.sed b/cgilite/html-sh.sed similarity index 100% rename from html-sh.sed rename to cgilite/html-sh.sed diff --git a/logging.sh b/cgilite/logging.sh similarity index 100% rename from logging.sh rename to cgilite/logging.sh diff --git a/markdown.awk b/cgilite/markdown.awk similarity index 100% rename from markdown.awk rename to cgilite/markdown.awk diff --git a/session.sh b/cgilite/session.sh similarity index 100% rename from session.sh rename to cgilite/session.sh diff --git a/storage.sh b/cgilite/storage.sh similarity index 100% rename from storage.sh rename to cgilite/storage.sh diff --git a/users.sh b/cgilite/users.sh similarity index 100% rename from users.sh rename to cgilite/users.sh diff --git a/index.cgi b/index.cgi new file mode 100755 index 0000000..c5b1652 --- /dev/null +++ b/index.cgi @@ -0,0 +1,125 @@ +#!/bin/sh + +USER_REGISTRATION=false +USER_REQUIREEMAIL=false + +. "${_EXEC:-${0%/*}}"/cgilite/cgilite.sh +. "$_EXEC"/cgilite/session.sh nocookie +. "$_EXEC"/cgilite/users.sh + +PATH_INFO="$(PATH "/${PATH_INFO#${_BASE}}")" + +export MD_HTML="false" +if [ "$(which awk)" ]; then + markdown() { awk -f "$_EXEC/cgilite/markdown.awk"; } +else + markdown() { busybox awk -f "$_EXEC/cgilite/markdown.awk"; } +fi + +checked(){ + local check="$1"; shift 1; + for comp in "$@"; do + if [ "$check" = "$comp" ] || [ "$check" -eq "$comp" ]; then + printf 'checked="checked"' + break; + fi 2>/dev/null + done +} +selected(){ + local check="$1"; shift 1; + for comp in "$@"; do + if [ "$check" = "$comp" ] || [ "$check" -eq "$comp" ]; then + printf 'selected="selected"' + break; + fi 2>/dev/null + done +} + +w_user_login(){ + if [ ! "$USER_ID" ]; then + cat <<-EOF + [form #user_login .login method=POST + [label Login] + [input name=uname placeholder="Username or Email" autocomplete=off] + [input type=password name=pw placeholder="Passphrase"] + [submit "action" "user_login" Login] + $([ "$USER_REGISTRATION" = true ] && printf '[a href="%s/register/" Register]' "$_BASE") + ] + EOF + elif [ "$USER_ID" ]; then + cat <<-EOF + [form #user_login .logout method=POST + [p Logged in as [span . $(HTML ${USER_NAME})]] + $([ "$USER_REGISTRATION" != true ] && printf '[a href="%s/invite/" Invite Friend]' "$_BASE") + [submit "action" "user_logout" Logout] + ] + EOF + fi +} + +yield_page(){ + title="${1:-RAW:NET}" page="$2" + printf '%s\r\n' 'Content-Type: text/html; charset=utf-8' \ + "Content-Security-Policy: script-src 'none'" \ + '' + { cat <<-EOF + [!DOCTYPE HTML] + [html [head + [meta name="viewport" content="width=device-width"] + [link rel="stylesheet" type="text/css" href="$_BASE/cgilite/common.css"] + [link rel="stylesheet" type="text/css" href="$_BASE/rawnet.css"] + [title . $(HTML "$title")] + ] [body class="$page" + [header + [form method=POST action="$_BASE/search/" + [input name=search placeholder="Search"] + ] + $(w_user_login) + ][main + EOF + cat + printf ']]]' + } |"$_EXEC/cgilite/html-sh.sed" -u +} + +case ${PATH_INFO} in + /favicon.ico) printf '%s\r\n' 'Content-Length: 0' '';; + *.css) + . "${_EXEC}/cgilite/file.sh" + FILE "${_EXEC}/${PATH_INFO}" + ;; + /login/) + if [ "$USER_ID" ]; then + REDIRECT "${_BASE}/" + else + yield_page 'RAW:NET Login' login <<-EOF + $(w_user_login) + EOF + fi + ;; + /register/) + yield_page 'RAW:NET Register User' register <<-EOF + $(w_user_register) + EOF + ;; + /recover/) + yield_page 'RAW:NET Recover Account' recover <<-EOF + $(w_user_recover) + EOF + ;; + /invite/) + yield_page 'RAW:NET Invite User' invite <<-EOF + $(w_user_invite) + EOF + ;; + /video/*/*.mp4|/video/*/*_thumb.jpg) + . "${_EXEC}/cgilite/file.sh" + FILE "${_DATA}/${PATH_INFO#/video/}" + ;; + /|/channel/*) . "${_EXEC}/page_channel.sh";; + /playlist/*) . "${_EXEC}/page_playlist.sh";; + /search/*) . "${_EXEC}/page_search.sh";; + *) . "${_EXEC}/page_404.sh";; +esac + +exit 0 diff --git a/page_404.sh b/page_404.sh new file mode 100755 index 0000000..a4acce8 --- /dev/null +++ b/page_404.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +printf 'Status: 404 Not Found\r\n' + +yield_page '' 404 <<-EOF +[h1 404][p +[span Page not found or nevermore] +[span Quoth the server: 404] +] +EOF diff --git a/page_channel.sh b/page_channel.sh new file mode 100755 index 0000000..8d1d183 --- /dev/null +++ b/page_channel.sh @@ -0,0 +1,265 @@ +#!/bin/sh + +chan_db="$_DATA/channels.db" + +channel='' video='' action='' +path_info="$PATH_INFO" +path_info="${path_info#/channel/}" +if [ "$(checkid "${path_info%%/*}")" ]; then + channel="${path_info%%/*}" + path_info="${path_info#*/}" +fi +if [ "$(checkid "${path_info%%/*}")" ]; then + video="${path_info%%/*}" + path_info="${path_info#*/}" +fi +action="${path_info}" +unset path_info + +# Channel +# ID NAME DESCRIPTION LOGO THEME AUTHORS DESCR_CACHE FUTUREUSE + +if [ "$channel" -a -f "$chan_db" -a -r "$chan_db" ]; then + read -r CHANNEL_ID CHANNEL_NAME CHANNEL_DESCRIPTION CHANNEL_LOGO \ + CHANNEL_THEME CHANNEL_AUTHORS CHANNEL_DESCR_CACHE \ + CHANNEL_FUTUREUSE <<-EOF + $(grep "^${channel} " "${chan_db}") + EOF + if [ "$CHANNEL_ID" ]; then + CHANNEL_NAME="$(UNSTRING "${CHANNEL_NAME}")" + CHANNEL_DESCRIPTION="$(UNSTRING "$CHANNEL_DESCRIPTION")" + CHANNEL_AUTHORS="$(UNSTRING "$CHANNEL_AUTHORS")" + CHANNEL_DESCR_CACHE="$(UNSTRING "$CHANNEL_DESCR_CACHE")" + vid_db="${_DATA}/${CHANNEL_ID}/videos.db" + else + channel='' + fi +fi + +update_channel(){ + local id="${1}" name description logo theme authors descr_cache futureuse + local ID NAME DESCRIPTION LOGO THEME AUTHORS DESCR_CACHE FUTUREUSE + local arg + + for arg in "$@"; do case $arg in + name=*) name="${arg#*=}";; + description=*) description="${arg#*=}";; + logo=*) logo="${arg#*=}";; + theme=*) theme="${arg#*=}";; + authors=*) authors="${arg#*=}";; + esac; done + + if LOCK "$chan_db"; then + while read -r ID NAME DESCRIPTION LOGO THEME AUTHORS DESCR_CACHE FUTUREUSE; do + if [ "$id" = "$ID" ]; then + printf '%s %s %s %s %s %s %s %s\n' \ + "$id" "$(STRING "${name-$(UNSTRING "$NAME")}")" \ + "$(STRING "${description-$(UNSTRING "$DESCRIPTION")}")" \ + "${logo:-${logo-${LOGO}}${logo+\\}}" \ + "${theme:-${theme-${THEME}}${theme+\\}}" \ + "$(STRING "${authors-$(UNSTRING "${AUTHORS}")}")" \ + "$(printf %s "${description-$(UNSTRING "$DESCRIPTION")}" |markdown |STRING)" \ + "${FUTUREUSE:-\\}" + else + printf '%s %s %s %s %s %s %s %s\n' \ + "$ID" "$NAME" "$DESCRIPTION" "$LOGO" "$THEME" "$AUTHORS" \ + "$DESCR_CACHE" "$FUTUREUSE" + fi + done <"$chan_db" >"${chan_db}.$$" + mv -- "${chan_db}.$$" "${chan_db}" + RELEASE "$chan_db" + else + return 1 + fi +} + +AUTHOR(){ + if [ "$CHANNEL_ID" -a "$USER_ID" -a ! "${CHANNEL_AUTHORS##*${USER_ID}*}" ]; then + return 0 + else + return 1 + fi +} + +# Video +# ID NAME DESCRIPTION RESX RESY LENGTH COVER STATUS UPLOADER HITS + +[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in + newchannel) + channel="$(POST channel |checkid)" + if [ ! "$USER_ID" ]; then + REDIRECT "${_BASE}/channel/#ERROR_NEWCHANNEL_NOTALLOWED" + elif LOCK "$chan_db"; then + if grep -q '^${channel} ' "$chan_db"; then + RELEASE "$chan_db" + REDIRECT "${_BASE}/channel/#ERROR_NEWCHANNEL_EXISTS" + else + printf '%s \\ \\ \\ \\ %s \\ \\\n' \ + "$channel" "$(STRING "$USER_ID")" \ + >>"$chan_db" + RELEASE "$chan_db" + REDIRECT "${_BASE}/channel/${channel}/edit" + fi + else + REDIRECT "${_BASE}/channel/#ERROR_NEWCHANNEL_NOLOCK" + fi + ;; + update_channel) + if [ ! "$channel" ]; then + REDIRECT "${_BASE}/channel/#ERROR_NOCHANNEL" + elif [ ! "$USER_ID" ]; then + REDIRECT "${_BASE}/channel/${channel}/#ERROR_NOTLOGGEDIN" + elif ! AUTHOR; then + REDIRECT "${_BASE}/channel/${channel}/#ERROR_UPDATE_NOTALLOWED" + elif update_channel "$channel" "name=$(POST name)" \ + "description=$(POST description)" \ + "authors=$USER_ID"; then + REDIRECT "${_BASE}/channel/${channel}/" + else + REDIRECT "${_BASE}/channel/${channel}/#ERROR_UPDATE_NOLOCK" + fi + ;; + update_channel_cancel) + REDIRECT "${_BASE}/channel/${channel}/" + ;; + newvideo) + video="$(POST video |checkid)" + + AUTHOR \ + && mkdir -p -- "${_DATA}/${channel}/" + + if [ ! "$video" ]; then + REDIRECT "${_BASE}/channel/${channel}/#ERROR_INVALID_ID" + elif [ ! "$channel" ]; then + REDIRECT "${_BASE}/channel/#ERROR_NOCHANNEL" + elif [ ! "$USER_ID" ]; then + REDIRECT "${_BASE}/channel/${channel}/#ERROR_NOTLOGGEDIN" + elif ! AUTHOR; then + REDIRECT "${_BASE}/channel/${channel}/#ERROR_UPDATE_NOTALLOWED" + elif LOCK "$vid_db"; then + if grep -q '^${video} ' "$vid_db"; then + RELEASE "$vid_db" + REDIRECT "${_BASE}/channel/${channel}/#ERROR_NEWVIDEO_EXISTS" + else + # ID NAME DESC RESX RESY LENGTH COVER STATUS UPLOADER HITS FUTUREUSE + printf '%s \\ \\ \\ \\ \\ \\ void %s \\ \\\n' \ + "$video" "$(STRING "$USER_ID")" \ + >>"$vid_db" + RELEASE "$vid_db" + REDIRECT "${_BASE}/channel/${channel}/${video}/edit" + fi + else + REDIRECT "${_BASE}/channel/${channel}/#ERROR_NEWVIDEO_NOLOCK" + fi + ;; +esac + +w_video(){ + local thumb + local VIDEO_ID VIDEO_NAME VIDEO_DESCRIPTION VIDEO_RESX VIDEO_RESY \ + VIDEO_LENGTH VIDEO_COVER VIDEO_STATUS VIDEO_UPLOADER VIDEO_HITS \ + VIDEO_DESCR_CACHE VIDEO_FUTUREUSE + + if read -r VIDEO_ID VIDEO_NAME VIDEO_DESCRIPTION VIDEO_RESX VIDEO_RESY \ + VIDEO_LENGTH VIDEO_COVER VIDEO_STATUS VIDEO_UPLOADER VIDEO_HITS \ + VIDEO_DESCR_CACHE VIDEO_FUTUREUSE; then + VIDEO_NAME="$(UNSTRING "$VIDEO_NAME")" + VIDEO_DESCRIPTION="$(UNSTRING "$VIDEO_DESCRIPTION")" + VIDEO_DESCR_CACHE="$(UNSTRING "$VIDEO_DESCR_CACHE")" + + [ "${VIDEO_STATUS}" = public ] || AUTHOR || return 0 + + thumb="${_BASE}/video/${CHANNEL_ID}/${VIDEO_ID}_thumb.jpg" + [ "$NAME" = \\ ] && NAME="(Unnamed Video)" + printf '[div .video .thumb + [h3 [a href="%s/channel/%s/%s/" . %s]] + [figure [img src="%s" alt=""]] + [span .duration . %i:%02i] + [div .description . %s] + ]' "$_BASE" "$CHANNEL_ID" "$VIDEO_ID" \ + "$(HTML "${VIDEO_NAME:-(Unnamed Video)}")" \ + "$thumb" \ + "$((${VIDEO_LENGTH%.*} / 60))" "$((${VIDEO_LENGTH%.*} % 60))" \ + "$(UNSTRING "$DESCR_CACHE")" + else + return 1 + fi +} + +w_channel(){ + local vid_db + local CHANNEL_ID CHANNEL_NAME CHANNEL_DESCRIPTION CHANNEL_LOGO \ + CHANNEL_THEME CHANNEL_AUTHORS CHANNEL_DESCR_CACHE CHANNEL_FUTUREUSE + + if read -r CHANNEL_ID CHANNEL_NAME CHANNEL_DESCRIPTION CHANNEL_LOGO \ + CHANNEL_THEME CHANNEL_AUTHORS CHANNEL_DESCR_CACHE \ + CHANNEL_FUTUREUSE; then + CHANNEL_NAME="$(UNSTRING "$CHANNEL_NAME")" + CHANNEL_DESCRIPTION="$(UNSTRING "$CHANNEL_DESCRIPTION")" + CHANNEL_AUTHORS="$(UNSTRING "$CHANNEL_AUTHORS")" + CHANNEL_DESCR_CACHE="$(UNSTRING "$CHANNEL_DESCR_CACHE")" + + vid_db="${_DATA}/${CHANNEL_ID}/videos.db" + cat <<-EOF + [div .channel + [div .description + [h2 [a href="${_BASE}/channel/${CHANNEL_ID}/" $(HTML "${CHANNEL_NAME:-(Unnamed Channel)}")]] + ${CHANNEL_DESCR_CACHE} + ]$( + [ -f "$vid_db" -a -r "$vid_db" ] \ + && while w_video; do :; done <"$vid_db" + ) + ] + EOF + else + return 1 + fi +} + +w_channel_list(){ + if [ $USER_ID ]; then + printf ' + [form .channel .newchannel method=POST + [hidden "channel" "%s"] + [submit "action" "newchannel" New Channel] + ]' "$(timeid)" + fi + [ -f "$chan_db" -a -r "$chan_db" ] \ + && while w_channel; do :; done <"$chan_db" +} + +if [ "$channel" -a "$video" ]; then + . ${_EXEC}/page_video.sh +elif [ "$channel" -a "$action" = edit ]; then + AUTHOR || REDIRECT "${_BASE}/${channel}/#ERROR_EDIT_NOTALLOWED" + yield_page "$CHANNEL_NAME - Edit" "channel edit" <<-EOF + [form .channel .edit method=POST + [input name="name" value="$(HTML "$CHANNEL_NAME")" placeholder="Channel Name"] + [textarea name="description" placeholder="Description" . $(HTML "$CHANNEL_DESCRIPTION")] + [submit "action" "update_channel" . Update] + [submit "action" "update_channel_cancel" . Cancel] + ] + EOF +elif [ "$channel" ]; then + yield_page "$CHANNEL_NAME" "channel" <<-EOF + [nav [a href="../" Channels] - [span $(HTML "${CHANNEL_NAME:-(Unnamed Channel)}")] + $(AUTHOR && printf ' - [a href="edit" edit]') + ] + [h1 .name $(HTML "$CHANNEL_NAME")] + [div .description . ${CHANNEL_DESCR_CACHE}] + [h1 .videos Videos] + [div .videos . $( + AUTHOR && printf ' + [form .video .newvideo method=POST + [hidden "video" "%s"] + [submit "action" "newvideo" New Video] + ]' "$(timeid)" + [ -f "$vid_db" -a -r "$vid_db" ] \ + && while w_video "$ID"; do :; done <"$vid_db" + )] + EOF +else + yield_page "Channels" "channels" <<-EOF + $(w_channel_list) + EOF +fi diff --git a/page_video.sh b/page_video.sh new file mode 100644 index 0000000..2961484 --- /dev/null +++ b/page_video.sh @@ -0,0 +1,246 @@ +#!/bin/sh + +# ID NAME DESCRIPTION RESX RESY LENGTH COVER STATUS (void|private|hidden|public) UPLOADER HITS DESCR_CACHE FUTUREUSE + +if [ "$video" -a -f "$vid_db" -a -r "$vid_db" ]; then + read -r VIDEO_ID VIDEO_NAME VIDEO_DESCRIPTION VIDEO_RESX VIDEO_RESY \ + VIDEO_LENGTH VIDEO_COVER VIDEO_STATUS VIDEO_UPLOADER VIDEO_HITS \ + VIDEO_DESCR_CACHE VIDEO_FUTUREUSE <<-EOF + $(grep "^${video} " "${vid_db}") + EOF + if [ "$VIDEO_ID" ]; then + VIDEO_NAME="$(UNSTRING "$VIDEO_NAME")" + VIDEO_DESCRIPTION="$(UNSTRING "$VIDEO_DESCRIPTION")" + VIDEO_COVER="$(UNSTRING "$VIDEO_COVER")" + VIDEO_DESCR_CACHE="$(UNSTRING "$VIDEO_DESCR_CACHE")" + else + video='' + fi +fi + +update_video(){ + local id="${1}" name description resx resy length cover status uploader \ + hits descr_cache futureuse + local ID NAME DESCRIPTION RESX RESY LENGTH COVER STATUS UPLOADER HITS \ + DESCR_CACHE FUTUREUSE + local arg video thumb cnt + video="${_DATA}/${CHANNEL_ID}/${VIDEO_ID}.mp4" + thumb="${_DATA}/${CHANNEL_ID}/${VIDEO_ID}_thumb.jpg" + + for arg in "$@"; do case $arg in + name=*) name="${arg#*=}";; + description=*) description="${arg#*=}";; + cover=*) cover="${arg#*=}";; + status=*) status="${arg#*=}";; + uploader=*) uploader="${arg#*=}";; + hits=*) hits="${arg#*=}";; + esac; done + + if [ -f "$video" -a -r "$video" ]; then + arg="$(echo; ffprobe -show_entries format=duration:stream=width,height "$video" 2>&-)" + resx="${arg#*width=}"; resx="${resx%%${BR}*}" + resy="${arg#*height=}"; resy="${resy%%${BR}*}" + length="${arg#*duration=}"; length="${length%%${BR}*}" + fi + if [ "${length%.*}" -a ! "${thumb}" -nt "${video}" ]; then + for cnt in 1 2 3 4 5 6 7 8 9 10; do + ffmpeg -nostdin -y -ss "$((cnt * ${length%.*} / 11))" -i "$video" \ + -frames 1 "${thumb%.jpg}_$((cnt - 1)).jpg" + done 2>&- + montage "${thumb%.jpg}"_[0-9].jpg \ + -background "#000000" \ + -tile 10x1 -geometry 320x180+0+0 \ + -interlace line -quality 85 "${thumb}" + rm -- "${thumb%.jpg}"_[0-9].jpg + fi + + if LOCK "$vid_db"; then + while read -r ID NAME DESCRIPTION RESX RESY LENGTH COVER STATUS UPLOADER HITS \ + DESCR_CACHE FUTUREUSE; do + if [ "$id" = "$ID" ]; then + printf '%s %s %s %i %i %f %s %s %s %i %s %s\n' \ + "$id" "$(STRING "${name-$(UNSTRING "$NAME")}")" \ + "$(STRING "${description-$(UNSTRING "$DESCRIPTION")}")" \ + "${resx:-${resx-${RESX}}${resx+0}}" \ + "${resy:-${resy-${RESY}}${resy+0}}" \ + "${length:-${length-${LENGTH}}${length+0}}" \ + "$(STRING "${cover-$(UNSTRING "$COVER")}")" \ + "${status:-${status-${STATUS}}${status+void}}" \ + "${uploader:-${uploader-${UPLOADER}}${uploader+\\}}" \ + "${hits:-${hits-${HITS}}${hits+0}}" \ + "$(printf %s "${description-$(UNSTRING "$DESCRIPTION")}" |markdown |STRING)" \ + "${FUTUREUSE:-\\}" + else + printf '%s %s %s %i %i %f %s %s %s %i %s %s\n' \ + "$ID" "$NAME" "$DESCRIPTION" "$RESX" "$RESY" "$LENGTH" \ + "$COVER" "$STATUS" "$UPLOADER" "$HITS" "$DESCR_CACHE" \ + "$FUTUREUSE" + fi + done <"$vid_db" >"${vid_db}.$$" + mv -- "${vid_db}.$$" "${vid_db}" + RELEASE "$vid_db" + else + return 1 + fi +} + +UPLOAD(){ + local file="$1" + local boundary line last + + [ ! "${CONTENT_TYPE}" -o "${CONTENT_TYPE##multipart/form-data;*}" ] && return 1 + + boundary="${CONTENT_TYPE#*; boundary=}" + boundary="${boundary%%;*}" + + head -c "$CONTENT_LENGTH" \ + | sed -nE ' + # discard lines prior to boundary + /^--'"${boundary}"'\r?$/!b; + # discard lines until first blank + :A; n; /^\r?$/!bA; n; + # print lines until boundary ( = actual file upload) + :FILE; p; n; + /^--'"${boundary}"'(--)?\r?$/!bFILE; + # discard remaining lines + :END; $q; n; bEND; + ' >"$file" + truncate -s $(( $(stat -c %s -- "$file") -2 )) -- "$file" +} + +[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in + update_video) + if [ ! "$USER_ID" ]; then + REDIRECT "${_BASE}/channel/${channel}/${video}/#ERROR_NOTLOGGEDIN" + elif ! AUTHOR; then + REDIRECT "${_BASE}/channel/${channel}/${video}/#ERROR_UPDATE_NOTALLOWED" + elif update_video "$video" "name=$(POST name)" \ + "description=$(POST description)" \ + "status=$(POST status |grep -m1 -xE 'void|private|hidden|public')" \ + "uploader=$USER_ID"; then + REDIRECT "${_BASE}/channel/${channel}/${video}/#UPDATE_SUCCESS" + else + REDIRECT "${_BASE}/channel/${channel}/${video}/#ERROR_UPDATE_NOLOCK" + fi + ;; + update_video_cancel) + REDIRECT "${_BASE}/channel/${channel}/${video}/#CANCELED" + ;; +esac + +if [ "$REQUEST_METHOD" = POST -a "$channel" -a "$video" ]; then + if ! AUTHOR; then + head -c "$CONTENT_LENGTH" >/dev/null + REDIRECT "${_BASE}/channel/${channel}/${video}/#ERROR_UPLOAD_NOTALLOWED" + elif [ "$VIDEO_STATUS" != void ]; then + head -c "$CONTENT_LENGTH" >/dev/null + REDIRECT "${_BASE}/channel/${channel}/${video}/#ERROR_UPLOAD_NOCLOBBER" + elif UPLOAD "$_DATA/$channel/$video.mp4"; then + update_video "$video" status=private + VIDEO_STATUS=private + fi +fi + +if [ "$channel" -a "$video" -a "$action" = edit ]; then + AUTHOR || REDIRECT "$_BASE/$channel/$video/#ERROR_EDIT_NOTALLOWED" + + yield_page "$VIDEO_NAME - Edit" "video edit" <<-EOF + [form .video .edit method=POST + [input name="name" value="$(HTML "$VIDEO_NAME")" placeholder="Video Name"] + [fieldset .status $([ $VIDEO_STATUS = void ] && printf "disabled=disabled") + [radio "status" "private" #status_private $(checked $VIDEO_STATUS private void)] + [label for=status_private tooltip="Video is only visible to channel authors" Private] + [radio "status" "hidden" #status_hidden $(checked $VIDEO_STATUS hidden)] + [label for=status_hidden tooltip="Video will not be listed but can be viewed by anyone knowing the URL" Hidden] + [radio "status" "public" #status_public $(checked $VIDEO_STATUS public)] + [label for=status_public tooltip="Video will be listed publicly" Public] + ] + [textarea name="description" placeholder="Description" . $(HTML "$VIDEO_DESCRIPTION")] + [submit "action" "update_video" . Update] + [submit "action" "update_video_cancel" . Cancel] + ] + EOF + +elif [ "$channel" -a "$video" -a "$action" = frameuploadprogress ]; then + AUTHOR || REDIRECT "$_BASE/$channel/$video/#ERROR_EDIT_NOTALLOWED" + printf '%s\r\n' 'Content-Type: text/html' 'Connection: close' '' + printf ' + + Upload Progress + + + ' + while [ "$VIDEO_STATUS" = void ]; do + printf '%i\n' "$(stat -c %s "$_DATA/$channel/$video.mp4" 2>&-)" + sleep 1 + read -r VIDEO_ID VIDEO_NAME VIDEO_DESCRIPTION VIDEO_RESX VIDEO_RESY \ + VIDEO_LENGTH VIDEO_COVER VIDEO_STATUS VIDEO_UPLOADER VIDEO_HITS \ + VIDEO_DESCR_CACHE VIDEO_FUTUREUSE <<-EOF + $(grep "^${video} " "${vid_db}") + EOF + done + printf 'Ready!\n' + printf '' + +elif [ "$channel" -a "$video" -a "$action" = frameupload ]; then + AUTHOR || REDIRECT "$_BASE/$channel/$video/#ERROR_EDIT_NOTALLOWED" + printf '%s\r\n' 'Content-Type: text/html' '' + [ "$VIDEO_STATUS" = void ] && "$_EXEC"/cgilite/html-sh.sed <<-EOF + [!DOCTYPE HTML] + [html [head + [title Upload Form] + ][body + [form .upload method=POST enctype="multipart/form-data" + [input type=file name=upload] + [submit "action" "video_upload" Upload] + ] + ]] + EOF + [ "$VIDEO_STATUS" != void ] && "$_EXEC"/cgilite/html-sh.sed <<-EOF + [!DOCTYPE HTML] + [html [head + [title Upload Form] + ][body + [a href="./" target="_parent" . Reload Page!] + ]] + EOF + +elif [ "$channel" -a "$video" ]; then + [ $VIDEO_STATUS = public -o $VIDEO_STATUS = hidden ] || AUTHOR || { . ${_EXEC}/page_404.sh; exit 0; } + + yield_page "$VIDEO_NAME" "video" <<-EOF + [nav [a href="../../" Channels] - [a href="../" $(HTML "${CHANNEL_NAME:-(Unnamed Channel)}")] - [span $(HTML "${VIDEO_NAME:-(Unnamed Video)}")] + $(AUTHOR && printf ' - [a href="edit" edit]') + ] + $( AUTHOR && [ $VIDEO_STATUS = void ] && printf ' + [iframe src="frameuploadprogress" width="100%%" height="50" + [a href="freameuploadprogress" Iframe: Upload progress] + ] + [iframe src="frameupload" width="100%%" height="50" + [form .upload method=POST enctype="multipart/form-data" + [input type=file name=upload] + [submit "action" "video_upload" Upload] + ] + ]') + $( [ $VIDEO_STATUS != void ] && printf ' + [video + [source src="%s/video/%s/%s.mp4"] + ]' "$_BASE" "$channel" "$video" + ) + [h1 .name $(HTML "$VIDEO_NAME")] + [div .description . ${VIDEO_DESCR_CACHE}] + EOF + +else + . "$_EXEC/page_404.sh" +fi diff --git a/rawnet.css b/rawnet.css new file mode 100644 index 0000000..a4b5548 --- /dev/null +++ b/rawnet.css @@ -0,0 +1,209 @@ +body { + background-position: right; + background-size: 4pt 4pt; + background-image: /* #6AF #6FF */ + linear-gradient( 0deg, transparent 25%, rgba(128,128,128,.5) 25% 50%, transparent 50% 75%, rgba(192,192,192,.5) 75%), + linear-gradient(90deg, transparent 25%, rgba(128,128,128,.5) 25% 50%, transparent 50% 75%, rgba(192,192,192,.5) 75%); +} + +header { + background: inherit; + padding: .25em 12em; + text-align: center; + box-shadow: #000 .25em .25em .25em; + z-index: 1; +} + +header > * { background: inherit; } +header:before, +header > *:before { + content: ''; position: absolute; + top: 0; right: 0; bottom: 0; left: 0; + background-color: rgba(0,0,0,.75); +} + +header a { color: #8CE; } + +header #user_login { + position: absolute; + right: 0; top: 31pt; max-height: 0; + width: 12em; + padding: 0 .5em; + text-align: center; + box-shadow: inherit; + transition: max-height linear .125s; +} + +#user_login > * { + position: relative; + top: -2.5em; +} +#user_login > *:last-child { + margin-bottom: -2em; +} +header #user_login:hover { + max-height: 10em; +} +header #user_login > p { + color: #EEE; + font-size: .875em; + line-height: 1.125em; +} +header #user_login > p span { + display: block; + font-size: initial; + line-height: 1.375em; +} +header #user_login label { + top: -1.5em; + font-size: 1.25em; + text-decoration: underline; + padding-bottom: 1em; + color: #EEE; + text-align: right; +} +header #user_login > * { + display: none; +} +header #user_login > :first-child, +header #user_login:hover > * { + display: block; + margin-left: auto; + margin-right: auto; +} +header #user_login:hover > a[href$="/register/"] { + text-align: right; + margin-top: .75em; +} + +main { + background-color: rgba(255,255,255,.75); + margin: 1em; padding: 1em; + box-shadow: #000 .125em .125em 1em; +} + +main nav { + font-size: .875em; + margin-top: -1em; +} +main nav > * { + padding: .125em .5em; + font-style: italic; + text-decoration: underline; +} + +body.channel main h1.name { + text-align: center; +} +body.channel main .description, +body.channel main form.edit, +body.video main form.edit { + max-width: 40em; + margin: auto; +} + +body.video main form.edit input[name=name], +body.video main form.edit textarea[name=description], +body.channel main form.edit input[name=name], +body.channel main form.edit textarea[name=description] { + display: block; + width: 100%; + margin-bottom: .5em; +} + +body.channels main .channel { + border: 1pt solid; + border-radius: 4pt; + padding: .5em; + margin-bottom: .5em; + height: 15em; + overflow: hidden; +} + +body.channels main .channel > .description { + overflow: hidden; +} +body.channels main .channel > .description h2 { + margin: 0; +} + +.channel > .description, .video.thumb, .newvideo { + display: inline-block; + vertical-align: top; + height: 14em; + width: 99%; margin: 0 .5%; + margin-bottom: 1em; +} + +.newvideo button { + display: block; + margin: 3em auto; +} + +.video.thumb:before, .newvideo:before { + content: ''; + position: absolute; + top: 0; left: 0; right:0; height: 11em; + box-shadow: #000 .25em .25em .5em; +} + +@media(min-width: 24em) { .channel > .description, .video.thumb, .newvideo { max-width: 49%; } } +@media(min-width: 44em) { .channel > .description, .video.thumb, .newvideo { max-width: 32%; } } +@media(min-width: 64em) { .channel > .description, .video.thumb, .newvideo { max-width: 24%; } } +@media(min-width: 84em) { .channel > .description, .video.thumb, .newvideo { max-width: 19%; } } +@media(min-width: 104em) { .channel > .description, .video.thumb, .newvideo { max-width: 19em; } } + +.video.thumb figure { + position: absolute; top: 0; + height: 11em; width: 100%; + overflow: hidden; +} +.video.thumb figure img { + position: absolute; top: 0; + height: 11em; min-width: 1000%; + background-color: #888; + max-width: unset; + margin-left: 50%; + transform: translate(-05%, 0); + object-fit: cover; +} +.video.thumb:hover img { + animation: thumbscroll 8s steps(10, end) infinite; +} +@keyframes thumbscroll { + from { transform: translate(-05%, 0);} + to { transform: translate(-105%, 0);} +} + +.video.thumb h3 { + position: absolute; + top: 10.25em; width: 100%; + height: 3em; + font-weight: bolder; + text-align: center; + word-break: break-word; + overflow: hidden; +} + +.video.thumb .duration { + position: absolute; + right: .375em; top: 10.625em; + font-size: .875em; + padding: 0 .25em; + background-color: #333;; + color: #EEE; + opacity: .75; +} + +.video.thumb .description { + position: absolute; + left:0; right:0; bottom: 3.5em; + font-size: .875em; + max-height: 1em; + background-color: rgba(0,0,0,.75); + color: #EEE; + transition: height linear .25s; +} +.video.thumb .description:hover { + max-height: 8em; +}