From: Paul Hänsch Date: Wed, 27 Oct 2021 20:39:41 +0000 (+0200) Subject: Merge commit 'a8e2ff19005524c1fcbd47a80e1cf0e675ed3f7a' X-Git-Url: https://git.plutz.net/?p=rawnet;a=commitdiff_plain;h=8b374fb64933f92041613b3eb625da593208d275;hp=a8e2ff19005524c1fcbd47a80e1cf0e675ed3f7a Merge commit 'a8e2ff19005524c1fcbd47a80e1cf0e675ed3f7a' --- 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/db_channel.sh b/db_channel.sh new file mode 100755 index 0000000..bb24961 --- /dev/null +++ b/db_channel.sh @@ -0,0 +1,113 @@ +#!/bin/sh + +[ "$include_dbchannel" ] && return 0 +include_dbchannel="$0" + +# == FILE FORMAT == +# ID NAME DESCRIPTION LOGO THEME AUTHORS DESCR_CACHE FUTUREUSE + +# == GLOBALS == +UNSET_CHANNEL='unset \ + CHANNEL_ID CHANNEL_NAME CHANNEL_DESCRIPTION CHANNEL_LOGO CHANNEL_THEME \ + CHANNEL_AUTHORS CHANNEL_DESCR_CACHE CHANNEL_FUTUREUSE +' + +LOCAL_CHANNEL='local \ + CHANNEL_ID CHANNEL_NAME CHANNEL_DESCRIPTION CHANNEL_LOGO CHANNEL_THEME \ + CHANNEL_AUTHORS CHANNEL_DESCR_CACHE CHANNEL_FUTUREUSE +' + +eval "$UNSET_CHANNEL" + +chan_db="$_DATA/channels.db" + +read_channel() { + local channel="$1" + + # Global exports + CHANNEL_ID='' CHANNEL_NAME='' CHANNEL_DESCRIPTION='' CHANNEL_LOGO='' + CHANNEL_THEME='' CHANNEL_AUTHORS='' CHANNEL_DESCR_CACHE='' + CHANNEL_FUTUREUSE='' + + if [ $# -eq 0 ]; then + read -r CHANNEL_ID CHANNEL_NAME CHANNEL_DESCRIPTION CHANNEL_LOGO \ + CHANNEL_THEME CHANNEL_AUTHORS CHANNEL_DESCR_CACHE \ + CHANNEL_FUTUREUSE + elif [ "$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 + fi + 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")" + else + eval "$UNSET_CHANNEL" + return 1 + 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 +} + +new_channel(){ + local channel="${1:-$(randomid)}" + + if LOCK "$chan_db"; then + if grep -q "^${channel} " "$chan_db"; then + RELEASE "$chan_db" + return 1 + fi + printf '%s \\ \\ \\ \\ %s \\ \\\n' \ + "$channel" "$(STRING "$USER_ID")" >>"$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 +} diff --git a/db_video.sh b/db_video.sh new file mode 100755 index 0000000..16bf434 --- /dev/null +++ b/db_video.sh @@ -0,0 +1,186 @@ +#!/bin/sh + +[ "$include_dbvideo" ] && return 0 +include_dbvideo="$0" + +# == FILE FORMAT == +# ID NAME DESCRIPTION RESX RESY LENGTH COVER STATUS UPLOADER HITS DESCR_CACHE FUTUREUSE +# (private|hidden|public) + +# == GLOBALS == +UNSET_VIDEO='unset \ + 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 \ + VIDEO_FILE VIDEO_THUMB VIDEO_MP4 VIDEO_WEBM +' + +LOCAL_VIDEO='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 \ + VIDEO_FILE VIDEO_THUMB VIDEO_MP4 VIDEO_WEBM +' + +eval "$UNSET_VIDEO" + +read_video() { + local video="$1" vid_db="$_DATA/$CHANNEL_ID/videos.db" + [ "$CHANNEL_ID" ] || return 1 + + # Global exports + 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='' + VIDEO_FILE='' VIDEO_THUMB='' VIDEO_MP4='' VIDEO_WEBM='' + + if [ $# -eq 0 ]; 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 + elif [ "$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 + fi + 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")" + + VIDEO_FILE="$_DATA/$CHANNEL_ID/${VIDEO_ID}.upload.mp4" + VIDEO_THUMB="$_DATA/$CHANNEL_ID/${VIDEO_ID}.thumb.jpg" + VIDEO_MP4="$_DATA/$CHANNEL_ID/${VIDEO_ID}.mp4" + VIDEO_WEBM="$_DATA/$CHANNEL_ID/${VIDEO_ID}.webm" + else + eval "$UNSET_VIDEO" + return 1 + fi +} + +update_video(){ + local id="${1:=${VIDEO_ID}}" 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 FILE THUMB MP4 WEBM arg cnt vid_db="$_DATA/$CHANNEL_ID/videos.db" + [ "$id" -a "$CHANNEL_ID" ] || return 1 + + FILE="$_DATA/$CHANNEL_ID/${id}.upload.mp4" + THUMB="$_DATA/$CHANNEL_ID/${id}.thumb.jpg" + # MP4="$_DATA/$CHANNEL_ID/${id}.mp4" + # WEBM="$_DATA/$CHANNEL_ID/${id}.webm" + + 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 "$FILE" -a -r "$FILE" ]; then + arg="$(echo; ffprobe -show_entries format=duration:stream=width,height "$FILE" 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 "${FILE}" ]; then + for cnt in 1 2 3 4 5 6 7 8 9 10; do + ffmpeg -nostdin -y -ss "$((cnt * ${length%.*} / 11))" -i "$FILE" \ + -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+private}}" \ + "${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 +} + +new_video(){ + local video="${1:-$(randomid)}" vid_db="$_DATA/$CHANNEL_ID/videos.db" + [ "$CHANNEL_ID" ] || return 1 + + if LOCK "$vid_db"; then + if grep -q "^${video} " "$vid_db"; then + RELEASE "$vid_db" + return 1 + fi + # ID NAME DESC RESX RESY LENGTH COVER STATUS UPLDR HITS FUTUREUSE + printf '%s \\ \\ 0 0 0 \\ private %s 0 \\\n' \ + "$video" "$(STRING "$USER_ID")" >>"$vid_db" + RELEASE "$vid_db" + else + return 1 + fi +} + +delete_video() { + local video="$1" vid_db="$_DATA/$CHANNEL_ID/videos.db" + [ "$CHANNEL_ID" ] || return 1 + + if LOCK "$vid_db"; then + grep -v "^${video} " <"$vid_db" >"${vid_db}.$$" + mv -- "${vid_db}.$$" "$vid_db" + RELEASE "$vid_db" + else + return 1 + fi +} + +list_videos(){ + local order="${1:-newest}" vid_db="$_DATA/$CHANNEL_ID/videos.db" + [ "$CHANNEL_ID" ] || return 1 + + [ -f "$vid_db" -a -r "$vid_db" ] && case "$order" in + name) + sort -k2 "$vid_db" + ;; + shortest) + sort -n -k6 "$vid_db" + ;; + longest) + sort -rn -k6 "$vid_db" + ;; + oldest) + cat "$vid_db" + ;; + newest|*) + tac "$vid_db" + ;; + esac +} diff --git a/index.cgi b/index.cgi new file mode 100755 index 0000000..76e3592 --- /dev/null +++ b/index.cgi @@ -0,0 +1,164 @@ +#!/bin/sh + +USER_REGISTRATION=false +USER_REQUIREEMAIL=false + +. "${_EXEC:-${0%/*}}"/cgilite/cgilite.sh +. "$_EXEC"/cgilite/session.sh nocookie +. "$_EXEC"/cgilite/users.sh + +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/) + if [ "$USER_ID" -a "$(GET user_register)" = confirm ]; then + printf 'Refresh: 2; url=%s\r\n' "/${_BASE#/}" + yield_page "RAW:NET Register confirm" "message register_confirm" <<-EOF + User registration successful! + EOF + exit 0 + fi + 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/) + if [ "$USER_ID" -a "$(GET user_register)" = confirm ]; then + printf 'Refresh: 2; url=%s\r\n' "/${_BASE#/}" + yield_page "RAW:NET Account activation" "message invite_confirm" <<-EOF + Account activation successful! + EOF + exit 0 + fi + yield_page 'RAW:NET Invite User' invite <<-EOF + [nav [a href="../" Channels] - [span Invite]] + $(w_user_invite) + EOF + ;; + /video/*/*.mp4|/video/*/*.webm|/video/*/*.jpg) + . "${_EXEC}/cgilite/file.sh" + FILE "${_DATA}/${PATH_INFO#/video/}" + ;; + /channel/*/*/*) + action="${PATH_INFO##*/}" + video="${PATH_INFO%/*}" video="${video##*/}" + channel="${PATH_INFO#/channel/}" channel="${channel%%/*}" + . "$_EXEC/page_video.sh" + ;; + /channel/*/*/) + action="" + video="${PATH_INFO%/}" video="${video##*/}" + channel="${PATH_INFO#/channel/}" channel="${channel%%/*}" + . "$_EXEC/page_video.sh" + ;; + /channel/*/*) + action="${PATH_INFO##*/}" + video="" + channel="${PATH_INFO#/channel/}" channel="${channel%%/*}" + . "$_EXEC/page_channel.sh" + ;; + /channel/*/) + action="" + video="" + channel="${PATH_INFO#/channel/}" channel="${channel%%/*}" + . "$_EXEC/page_channel.sh" + ;; + /|/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..36d9dcd --- /dev/null +++ b/page_channel.sh @@ -0,0 +1,101 @@ +#!/bin/sh + +["$includepage_channel" ] && return 0 +includepage_channel="$0" + +. "$_EXEC/db_channel.sh" +. "$_EXEC/widgets.sh" + +read_channel "$channel" + +[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in + newchannel) + channel="$(POST channel |checkid)" + if [ ! "$USER_ID" ]; then + REDIRECT "${_BASE}/channel/#ERROR_NEWCHANNEL_NOTALLOWED" + elif new_channel "$channel"; then + REDIRECT "${_BASE}/channel/$channel/edit" + else + REDIRECT "${_BASE}/channel/#ERROR_NEWCHANNEL_NOLOCK" + fi + ;; + update_channel) + if [ ! "$CHANNEL_ID" ]; then + REDIRECT "${_BASE}/channel/#ERROR_NOCHANNEL" + elif [ ! "$USER_ID" ]; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/#ERROR_NOTLOGGEDIN" + elif ! AUTHOR; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/#ERROR_UPDATE_NOTALLOWED" + elif update_channel "$CHANNEL_ID" "name=$(POST name)" \ + "description=$(POST description)" \ + "authors=$USER_ID"; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/" + else + REDIRECT "${_BASE}/channel/$CHANNEL_ID/#ERROR_UPDATE_NOLOCK" + fi + ;; + update_channel_cancel) + REDIRECT "${_BASE}/channel/$CHANNEL_ID/" + ;; + newvideo) + . "$_EXEC/db_video.sh" + video="$(POST video |checkid)" + + AUTHOR \ + && mkdir -p -- "${_DATA}/$CHANNEL_ID/" + + if [ ! "$video" ]; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/#ERROR_INVALID_ID" + elif [ ! "$CHANNEL_ID" ]; then + REDIRECT "${_BASE}/channel/#ERROR_NOCHANNEL" + elif [ ! "$USER_ID" ]; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/#ERROR_NOTLOGGEDIN" + elif ! AUTHOR; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/#ERROR_UPDATE_NOTALLOWED" + elif new_video "$video"; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$video/" + else + REDIRECT "${_BASE}/channel/$CHANNEL_ID/#ERROR_NEWVIDEO_NOLOCK" + fi + ;; +esac + +if [ "$CHANNEL_ID" -a "$action" = edit ]; then + AUTHOR || REDIRECT "${_BASE}/$CHANNEL_ID/#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" autocomplete=off] + [textarea name="description" placeholder="Description" . $(HTML "$CHANNEL_DESCRIPTION")] + [submit "action" "update_channel" . Update] + [submit "action" "update_channel_cancel" . Cancel] + ] + EOF +elif [ "$CHANNEL_ID" ]; then + yield_page "$CHANNEL_NAME" "channel" <<-EOF + [nav [a href="../" Channels] - [span $(HTML "${CHANNEL_NAME:-(Unnamed Channel)}")]] + [h1 .name $(HTML "$CHANNEL_NAME")] + [div .description . ${CHANNEL_DESCR_CACHE}] + $(AUTHOR && printf '[a .button href="edit" edit]') + [h1 .videos Videos] + [div .videos . $( + AUTHOR && printf ' + [form .video .newvideo method=POST + [hidden "video" "%s"] + [submit "action" "newvideo" New Video] + ]' "$(timeid)" + list_videos |while w_video; do :; done + )] + EOF +else + yield_page "Channels" "channels" <<-EOF + $([ "$USER_ID" ] && printf ' + [form .channel .newchannel method=POST + [hidden "channel" "%s"] + [submit "action" "newchannel" New Channel] + ]' "$(timeid)" + ) + $([ -f "$chan_db" -a -r "$chan_db" ] \ + && while w_channel; do :; done <"$chan_db" + ) + EOF +fi diff --git a/page_video.sh b/page_video.sh new file mode 100755 index 0000000..9e49a6f --- /dev/null +++ b/page_video.sh @@ -0,0 +1,128 @@ +#!/bin/sh + +[ "$includepage_video" ] && return 0 +includepage_video="$0" + +. "$_EXEC/db_channel.sh" +. "$_EXEC/db_video.sh" +. "$_EXEC/upload.sh" + +read_channel "$channel" +read_video "$video" + +[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in + update_video) + if [ ! "$USER_ID" ]; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#ERROR_NOTLOGGEDIN" + elif ! AUTHOR; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#ERROR_UPDATE_NOTALLOWED" + elif update_video "$VIDEO_ID" "name=$(POST name)" \ + "description=$(POST description)" \ + "status=$(POST status |grep -m1 -xE 'private|hidden|public')" \ + "uploader=$USER_ID"; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#UPDATE_SUCCESS" + else + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#ERROR_UPDATE_NOLOCK" + fi + ;; + update_video_cancel) + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#CANCELED" + ;; + delete) + if [ ! "$USER_ID" ]; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#ERROR_NOTLOGGEDIN" + elif ! AUTHOR; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#ERROR_UPDATE_NOTALLOWED" + elif [ "$(POST delconfirm)" != confirm ]; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#ERROR_NOT_CONFIRMED" + elif delete_video "$VIDEO_ID"; then + rm -f -- "$VIDEO_FILE" "$VIDEO_THUMB" "$VIDEO_MP4" "$VIDEO_WEBM" + REDIRECT "${_BASE}/channel/$CHANNEL_ID/#DELETE_CONFIRM" + else + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#ERROR_UPDATE_NOLOCK" + fi + ;; +esac + +if [ "$REQUEST_METHOD" = POST -a "$CHANNEL_ID" -a "$VIDEO_ID" ]; then + if ! AUTHOR; then + head -c "$CONTENT_LENGTH" >/dev/null + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#ERROR_UPLOAD_NOTALLOWED" + elif [ -f "$VIDEO_FILE" ]; then + head -c "$CONTENT_LENGTH" >/dev/null + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/#ERROR_UPLOAD_NOCLOBBER" + elif UPLOAD "$VIDEO_FILE"; then + REDIRECT "${_BASE}/channel/$CHANNEL_ID/$VIDEO_ID/edit" + fi +fi + +if [ "$CHANNEL_ID" -a "$VIDEO_ID" -a "$action" = edit ]; then + AUTHOR || REDIRECT "$_BASE/$CHANNEL_ID/$VIDEO_ID/#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" autocomplete=off] + [fieldset .status $([ ! -f "$VIDEO_FILE" ] && printf "disabled=disabled") + [radio "status" "private" #status_private $(checked $VIDEO_STATUS private)] + [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] + [fieldset .delete + [checkbox "delconfirm" "confirm" id="delconfirm"] + [label for=delconfirm Delete Video] + [submit "action" "delete" Delete Video] + ] + ] + EOF + +elif [ "$CHANNEL_ID" -a "$VIDEO_ID" -a "$action" = frameuploadprogress ]; then + AUTHOR || REDIRECT "$_BASE/$CHANNEL_ID/$VIDEO_ID/#ERROR_EDIT_NOTALLOWED" + printf '%s\r\n' 'Content-Type: text/html' 'Connection: close' '' + frame_uploadprogress + +elif [ "$CHANNEL_ID" -a "$VIDEO_ID" -a ! -f "$VIDEO_FILE" ] && AUTHOR; then + yield_page "$VIDEO_NAME" "video" <<-EOF + [nav [a href="../../" Channels] - [a href="../" $(HTML "${CHANNEL_NAME:-(Unnamed Channel)}")] - [span $(HTML "${VIDEO_NAME:-(Unnamed Video)}")]] + [iframe src="frameuploadprogress" width="100%%" height="50" + [a href="freameuploadprogress" Iframe: Upload progress] + ] + [form .upload method=POST enctype="multipart/form-data" + [input type=file name=upload] + [submit "action" "video_upload" Upload] + ] + [a .button href="edit" edit] + [h1 .name $(HTML "$VIDEO_NAME")] + [div .description . ${VIDEO_DESCR_CACHE}] + EOF + +elif [ "$CHANNEL_ID" -a "$VIDEO_ID" -a -f "$VIDEO_FILE" ]; 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)}")]] + [video preload=none controls=controls width="$VIDEO_RESX" height="$VIDEO_RESY" + $([ -f "$VIDEO_MP4" ] \ + && printf '[source src="%s/video/%s/%s.mp4" type="video/mp4"]' \ + "$_BASE" "$CHANNEL_ID" "$VIDEO_ID" + [ -f "$VIDEO_WEBM" ] \ + && printf '[source src="%s/video/%s/%s.webm" type="video/webm"]' \ + "$_BASE" "$CHANNEL_ID" "$VIDEO_ID" + [ ! -f "$VIDEO_MP4" -a ! -f "$VIDEO_WEBM" ] \ + && printf '[source src="%s/video/%s/%s.upload.mp4" type="video/mp4"] %s' \ + "$_BASE" "$CHANNEL_ID" "$VIDEO_ID" \ + "The video has not yet been transcoded and may not be displayed correctly." + )] + $(AUTHOR && printf '[a .button href="edit" edit]') + [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..650da60 --- /dev/null +++ b/rawnet.css @@ -0,0 +1,270 @@ +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 main .description, +body main form, +body main iframe { + display: block; + max-width: 40em; + margin: auto; +} + +body main form input[name=name], +body main form input[name=email], +body main form textarea { + 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; + margin-right: -.5%; +} +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 > a { + display: block; + color: inherit; + font-style: inherit; + text-decoration: inherit; +} + +.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; +} + +body.video.edit form > .delete { + text-align: right; +} +body.video.edit form #delconfirm + label:after { + content: '\0A'; + white-space: pre; +} +body.video.edit form #delconfirm + label + button { + pointer-events: none; + color: #AAA; + border-color: #AAA; + left: -.75em; +} +body.video.edit form #delconfirm:checked + label + button { + pointer-events: auto; + color: inherit; + border-color: inherit; + background-color: #FDD; +} + +body.video video { + display: block; + margin: 0 auto; + max-height: 80vh; +} + +#uploadprogress { + text-align: center; + background: transparent; + margin: 0; +} +#uploadprogress .progress { + display: block; + position: absolute; + width: 99%; width: calc(100% - 2pt); + background-color: #FFF; + border: 1pt solid; + border-radius: 4pt; + height: 1.25em; +} +#uploadprogress .progress .bar { + display: block; + position: absolute; + left: 0; top: 0; bottom: 0; + background-color: #666; +} +#uploadprogress .progress .count { + display: block; + position: absolute; + left: 0; top: 0; right: 0; bottom: 0; + line-height: 1.375em; +} diff --git a/upload.sh b/upload.sh new file mode 100755 index 0000000..f9e4296 --- /dev/null +++ b/upload.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +[ "$include_upload" ] && return 0 +include_upload="$0" + +UPLOAD(){ + local file="$1" + local boundary line length=0 + + [ ! "${CONTENT_TYPE}" -o "${CONTENT_TYPE##multipart/form-data;*}" ] && return 1 + + boundary="${CONTENT_TYPE#*; boundary=}" + boundary="${boundary%%;*}" + + while read -r line; do + length="$(( length + ${#line} + 1))" + [ "${line%${CR}}" = "--$boundary" ] && break + done + while read -r line; do + length="$(( length + ${#line} + 1))" + [ ! "${line%${CR}}" ] && break \ + || debug "$line" + done + + printf "%i\n" "$(( CONTENT_LENGTH - length ))" >"${file}.upload" + head -c "$(( CONTENT_LENGTH - length ))" \ + | sed -nE ' + # 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" + rm -- "${file}.upload" +} + +frame_uploadprogress() { + printf ' + + Upload Progress + + + ' "$_BASE" + printf '
%i / %i
\n' 0 0 + while [ ! -f "${VIDEO_FILE}" -a ! -f "${VIDEO_FILE}.upload" ]; do + sleep 1 + done + read size <"${VIDEO_FILE}.upload" + while [ -f "${VIDEO_FILE}.upload" ]; do + stat="$(stat -c %s "$VIDEO_FILE" 2>&-)" + printf '
%iMB / %iMB
\n' \ + "$(( stat * 100 / size ))" "$((stat / 1048576))" "$((size / 1048576))" + sleep 1 + done + printf '
Ready!
\n' + printf '' +} diff --git a/widgets.sh b/widgets.sh new file mode 100755 index 0000000..b951216 --- /dev/null +++ b/widgets.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +["$include_widgets" ] && return 0 +include_widgets="$0" + +. "$_EXEC/db_channel.sh" +. "$_EXEC/db_video.sh" + +w_video(){ + local thumb + eval "$LOCAL_VIDEO" + + if read_video; then + VIDEO_LENGTH="${VIDEO_LENGTH%.*}" + [ "${VIDEO_STATUS}" = public ] || AUTHOR || return 0 + + thumb="${_BASE}/video/${CHANNEL_ID}/${VIDEO_ID}.thumb.jpg" + [ "$NAME" = \\ ] && NAME="(Unnamed Video)" + printf '[div .video .thumb + [a href="%s/channel/%s/%s/" + [h3 . %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(){ + eval "$LOCAL_CHANNEL" + + if read_channel; then + cat <<-EOF + [div .channel + [div .description + [h2 [a href="${_BASE}/channel/${CHANNEL_ID}/" $(HTML "${CHANNEL_NAME:-(Unnamed Channel)}")]] + ${CHANNEL_DESCR_CACHE} + ] + $( list_videos |while w_video; do :; done; ) + ] + EOF + else + return 1 + fi +}