From: Andy Spencer Date: Mon, 7 Jan 2013 05:32:57 +0000 (+0000) Subject: Add view command, embedding, and caching X-Git-Url: http://pileus.org/git/?p=vpaste;a=commitdiff_plain;h=be9a79188b57c09b9232ab2dc6984b1f938dd3ee Add view command, embedding, and caching This is mostly related to embedding pastes in external pages. Script tags can be used to link an existing paste, or format a new paste based on the text enclosed in the script tag. Code tags can be converted to pastes as well. When formatting text that is not in an existing paste, the text is not saved and is not shown on the main page. This is done using the view command. Additionally, caching was added to improve load times, specifically for text enclosed in script/code tags. The TOhtml output is saved in a file based on the md5sum of the text and some system version information. A new response function was also added to simplify some processing. It combines the message and header functions and supports additional things like gzipped input files (for cached pages) and Access Control headers. --- diff --git a/.gitignore b/.gitignore index b085d33..eaf04f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *~ *.swp favicon.ico +cache/* db/* spam/* diff --git a/embed.js b/embed.js new file mode 100644 index 0000000..d949c42 --- /dev/null +++ b/embed.js @@ -0,0 +1,154 @@ +// Copyright (C) 2013 Andy Spencer +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. + +/* Constants */ +var vpaste = 'http://vpaste.net/'; + +var light = '#f4f4f4'; +var dark = '#111133'; +var border = '#8888dd'; + +var styles = [ + 'background: ' + light + ';', + 'border: solid 1px ' + border + ';', + 'padding: 0.5em;', + 'overflow: auto;', + 'display: block;', + 'white-space: pre;', + 'text-align: left;', +]; + +/* Globals */ +var query; + +/* Update Style Sheet */ +function update_styles(style, name) { + var text = style.innerHTML; + var lines = text.split(/\n/); + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + line = line.replace(/#ffffff/, light); + line = line.replace(/#000000/, dark); + if (line.match(/^pre/)) + line = line.replace(/^pre/, '.'+name); + else if (line.match(/^\./)) + line = '.'+name+' '+line; + else + line = ''; + lines[i] = line; + } + style.innerHTML = lines.join('\n'); +} + +/* Embed paste into page */ +function embed_paste(ajax, style, html, name) { + if (ajax.readyState != 4 && ajax.readyState != 'complete') + return; + if (!ajax.responseXML) + throw new Error('No response XML: ' + ajax.responseText); + var xml = ajax.responseXML; + var vstyle = xml.getElementsByTagName('style')[0]; + var vhtml = xml.getElementsByTagName('pre')[0]; + update_styles(vstyle, name); + style.innerHTML += vstyle.innerHTML; + html.innerHTML = vhtml.innerHTML.replace(/^\n*/, ''); + html.style.visibility = 'visible'; +} + +/* Strip whitespace from a paste */ +function strip_spaces(text) { + var prefix = null; + var lines = text.replace(/^\s*\n|\n\s*$/g,'').split('\n'); + for (i in lines) { + if (lines[i].match(/^\s*$/)) + continue; + var white = lines[i].replace(/\S.*/, '') + if (prefix === null || white.length < prefix.length) + prefix = white; + } + for (i in lines) + lines[i] = lines[i].replace(prefix, ''); + return lines.join('\n'); +} + +/* Start embedding a paste */ +function query_paste(method, url, body, html, name) { + /* Add style tag box */ + var style = document.createElement('style'); + style.type = "text/css"; + style.innerHTML = '.' + name + ' { ' + styles.join(' ') + ' }'; + document.head.insertBefore(style, document.head.firstChild); + + /* Get AJAX object */ + var ajax = null; + if (!ajax) ajax = new XMLHttpRequest(); + if (!ajax) ajax = new ActiveXObject('Microsoft.XMLHTTP'); + if (!ajax) throw new Error('Cannot get AJAX object'); + + /* Insert default query */ + if (query) + url = url.replace(/[?]/, '?' + query + ','); + + /* Run AJAX Request */ + ajax.onreadystatechange = function() { + embed_paste(ajax, style, html, name) }; + ajax.open(method, url, true); + ajax.setRequestHeader('Accept', 'text/html'); + ajax.overrideMimeType('application/xhtml+xml'); + ajax.send(body); +} + +/* Start embedding a paste */ +function start_embed() { + /* Get current paste information */ + var scripts = document.getElementsByTagName('script'); + var script = scripts[scripts.length-1]; + var text = strip_spaces(script.innerHTML); + var name = 'vpaste_s' + scripts.length; + var regex = /^[^?]*[?]?(([a-zA-Z0-9.]*)[?&, ]?(.*))$/; + var parts = script.src.match(regex); + + /* Handle header tags */ + if (!text && !parts[2] || !document.body) + return query = parts[1]; + + /* Add paste box */ + var html = document.createElement('pre'); + html.innerHTML = 'Loading..'; + html.className = script.className + ' vpaste ' + name; + document.body.appendChild(html); + + /* Query the paste */ + if (text) + query_paste('POST', vpaste+'view?'+parts[1], + text, html, name); + else + query_paste('GET', vpaste+parts[2]+'?'+parts[3], + null, html, name); +} + +/* Convert all code tags to pastes */ +function format_code(type) { + var tags = document.getElementsByTagName('code'); + for (var i = 0; i < tags.length; i++) { + var tag = tags[i]; + if (type && tag.className != type) + continue; + var name = 'vpaste_c' + i; + var text = strip_spaces(tag.innerHTML); + tag.className += ' ' + name; + query_paste('POST', vpaste+'view?'+tag.title, + text, tag, name); + } +} + +start_embed(); diff --git a/index.cgi b/index.cgi index aacc875..2fea18e 100755 --- a/index.cgi +++ b/index.cgi @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2009-2012 Andy Spencer +# Copyright (C) 2009-2013 Andy Spencer # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free @@ -44,31 +44,114 @@ function cut_file { ' | head -c $((128*1024)) # Limit size to 128K } -# Print out a generic header -function header { - echo "Content-Type: $1; charset=UTF-8" +# Respond to a request +function respond { + local allow ctype heads files texts gzip h f + while [ "$1" ]; do + case $1 in + -y) allow="true"; ;; + -c) ctype="$2"; shift ;; + -h) heads=("${heads[@]}" "$2"); shift ;; + -f) files=("${files[@]}" "$2"); shift ;; + *) texts=("${texts[@]}" "$1"); ;; + esac + shift + done + + # Check if the browser supports gzip if [[ "$HTTP_ACCEPT_ENCODING" == *'gzip'* ]]; then + gzip=true + fi + + # Output header + if [ "$ctype" ]; then + echo "Content-Type: $ctype; charset=UTF-8" + else + echo "Content-Type: text/plain; charset=UTF-8" + fi + if [ "$gzip" ]; then echo "Content-Encoding: gzip" - echo + fi + if [ "$allow" ]; then + echo "Access-Control-Allow-Origin: *" + echo "Access-Control-Allow-Headers: Content-Type" + fi + for h in "${heads[@]}"; do + echo "$h" + done + echo + + # Output text messages + if [ "$texts" ]; then + if [ "$gzip" ]; then + echo "${texts[@]}" | gzip + else + echo "${texts[@]}" + fi + exit + fi + + # Output body files + if [ "$files" ]; then + for f in "${files[@]}"; do + if [[ "$gzip" && "$f" != *'.gz' ]]; then + gzip < "$f" + elif [[ ! "$gzip" && "$f" == *'.gz' ]]; then + zcat < "$f" + else + cat "$f" + fi + done + exit + fi + + # Gzip remaining stream + if [ "$gzip" ]; then exec 1> >(gzip) - else - echo fi } -# Print plain message and exit -function message { - while [ "$1" == '-h' ]; do - shift; echo "$1"; shift - done - header text/plain - echo "$*" - exit +# Format and output a file +function format { + # Create a temp file with the provided modeline + tmp="$(mktemp)" + sed "\$avim: $(get_modeline)" "$1" > "$tmp" + + # Determine cache name + md5="$(cat index.cgi vimrc "$tmp" /usr/bin/ex | md5sum -b)" + out="cache/${md5% *}.htm" + zip="$out.gz" + + # Cache the file, if needed + if [ ! -f "$zip" ]; then + # - I have some plugins in ~/.vim + # - Run ex in pty to trick it into thinking that it + # has a real terminal, note that we also have to set + # term=xterm-256color in vimrc + HOME=/home/andy \ + /home/vpaste/bin/pty \ + /usr/bin/ex -nXZ -i NONE -u vimrc \ + '+sil! set fde= fdt= fex= inde= inex= key= pa= pexpr=' \ + '+sil! set iconstring= ruf= stl= tal=' \ + "+sil! set titlestring=$1\ -\ vpaste.net" \ + '+sil! set noml' \ + '+sil! $d|'$2 \ + '+sil! %s/\r//g' \ + '+sil! TOhtml' \ + "+sav! $out" \ + '+qall!' \ + "$tmp" >/dev/null 2>&1 + gzip "$out" + fi + rm "$tmp" + + # Output the file + respond -y -c "text/html" -f "$zip" } # List previous pastes function do_cmd { - header text/plain + respond case "$1" in ls) ls -t db | column @@ -106,47 +189,34 @@ function do_print { input="db/$1" trim='1,/^$/d' # sed command to remove cruft else - message -h 'Status: 404 Not Found' \ + respond -h 'Status: 404 Not Found' \ "File '$1' not found" fi + # Check for javascript + if [[ "$input" == 'embed.js' && + "$HTTP_ACCEPT" != *'html'* ]]; then + respond -c text/javascript -f "$input" + fi + # Check for raw paste if [[ "$QUERY_STRING" == 'raw'* || "$REQUEST_URI" != *'?'* && ( "$input" != 'db/'* || "$HTTP_ACCEPT" != *'html'* ) ]]; then - header text/plain + respond sed "$trim" "$input" exit fi - # Create a temp file with the provided modeline - output="$(mktemp)" - tmp="$(mktemp)" - sed "\$avim: $(get_modeline)" "$input" > "$tmp" - - # - I have some plugins in ~/.vim - # - Run ex in screen to trick it into thinking that it - # has a real terminal, note that we also have to set - # term=xterm-256color in vimrc - HOME=/home/andy \ - screen -D -m ex -nXZ -i NONE -u vimrc \ - '+sil! set fde= fdt= fex= inde= inex= key= pa= pexpr=' \ - '+sil! set iconstring= ruf= stl= tal=' \ - "+sil! set titlestring=$1\ -\ vpaste.net" \ - '+sil! set noml' \ - '+sil! $d|'$trim \ - '+sil! %s/\r//g' \ - '+sil! TOhtml' \ - "+sav! $output" \ - '+qall!' \ - "$tmp" - - header text/html - cat "$output" - rm "$tmp" "$output" + # Output the file + format "$input" "$trim" } +# Format a file for viewing +function do_view { + format - +} # Upload handler function do_upload { @@ -154,9 +224,9 @@ function do_upload { spam=$(echo -n "$body" | cut_file "ignoreme") text=$(echo -n "$body" | cut_file "(text|x)") bans=$(echo -n "$REMOTE_ADDR" | grep -f blacklist) - [ ! -z "$spam" ] && message "Spam check.." - [ ! -z "$bans" ] && message "You have been banned" - [ -z "$text" ] && message "No text pasted" + [ ! -z "$spam" ] && respond "Spam check.." + [ ! -z "$bans" ] && respond "You have been banned" + [ -z "$text" ] && respond "No text pasted" # Format and save message output="$(mktemp db/XXXXX)" @@ -171,7 +241,7 @@ function do_upload { # Redirect user uri="$url$(basename "$output")" - message -h 'Status: 302 Found' \ + respond -h 'Status: 302 Found' \ -h "Location: $uri" \ "$uri" } @@ -188,7 +258,7 @@ function do_help { vpaste='vpaste' repo='https://lug.rose-hulman.edu/svn/misc/trunk/htdocs/vpaste/' - header text/html + respond -c text/html cat <<-EOF @@ -313,14 +383,15 @@ function do_help {

License

-

Copyright © 2009-2012 +

Copyright © 2009-2013 Andy Spencer <andy753421@gmail.com>

See individual files for licenses

Source code

Client
-
vpaste
+
vpaste + embed.js
Server
index.cgi vimrc @@ -372,6 +443,8 @@ elif [ "$pathinfo" = head ]; then do_cmd head elif [ "$pathinfo" = stat ]; then do_cmd stat +elif [ "$pathinfo" = view ]; then + do_view elif [ "$pathinfo" ]; then do_print "$pathinfo" elif [ "$CONTENT_TYPE" ]; then diff --git a/vimrc b/vimrc index 074722e..ac81a9e 100644 --- a/vimrc +++ b/vimrc @@ -1,4 +1,4 @@ -" Andy Spencer 2009-2011 - Public domain +" Andy Spencer 2009-2013 - Public domain filetype plugin indent on syntax on @@ -20,6 +20,7 @@ let g:html_use_css = 1 let g:html_use_encoding = "UTF-8" let g:html_no_progress = 1 let g:html_dynamic_folds = 1 +let g:html_use_xhtml = 1 " Misc let g:is_bash = 1