First commit

This commit is contained in:
Matthew Connelly 2015-04-13 03:56:29 +01:00
commit 7a3a80c791
16 changed files with 650 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
*~
*.swp
blib
Makefile
Makefile.old
.build
*.db
_Inline
data/pastes.db

13
README.md Normal file
View File

@ -0,0 +1,13 @@
App::WerePaste
--------------
Perl-based pastebin software, largely a rewrite of Dancebin, using Pygments via Inline::Python for syntax highlighting.
Requirements:
- Dancer2
- Dancer2::Plugin::DBIC
- Data::UUID
- Try::Tiny
- Inline::Python
- pip module Pygments

9
bin/app.pl Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../lib";
use App::WerePaste;
App::WerePaste->start;

167
config.yml Normal file
View File

@ -0,0 +1,167 @@
appname: "WerePaste"
layout: "main"
charset: "UTF-8"
tz: "Europe/London"
locale: "en_GB"
logger: "console"
log: "debug"
template: "template_toolkit"
engines:
template:
template_toolkit:
encoding: 'utf8'
start_tag: '[%'
end_tag: '%]'
plugins:
DBIC:
default:
schema_class: 'App::WerePaste::Schema'
dsn: dbi:SQLite:dbname=data/pastes.db
options:
sqlite_unicode: 1
# Comment out for production
show_errors: 1
# Sets the expiration for posts
# Options can be any combination of weeks, days, hours, minutes, seconds
expiration:
weeks: 2
# Languages supported by Pygments
languages:
- Programming languages:
- ActionScript:
- Ada:
- ANTLR:
- AppleScript:
- Assembly: nasm
- Asymptote:
- Awk:
- Bash:
- Befunge:
- Boo:
- BrainFuck:
- C:
- C++:
- C#:
- Clojure:
- CoffeeScript:
- ColdFusion: cfm
- Common Lisp:
- Coq:
- Cryptol:
- Cython:
- D:
- Dart:
- Delphi:
- Dylan:
- Erlang:
- Factor:
- Fancy:
- Fortran:
- F#:
- GAP:
- Gherkin:
- GL shaders:
- Groovy:
- Haskell:
- IDL:
- Io:
- Java:
- JavaScript:
- Lasso:
- LLVM:
- Logtalk:
- Lua:
- Matlab:
- MiniD:
- Modelica:
- Modula-2:
- MuPad:
- Nemerle:
- Nimrod:
- Objective-C:
- Objective-J:
- Octave:
- OCaml:
- PHP:
- Perl:
- PovRay:
- PostScript:
- PowerShell:
- Prolog:
- Python:
- REBOL:
- Red:
- Redcode:
- Ruby:
- Rust:
- R:
- S:
- S-Plus:
- Scala:
- Scheme:
- Scilab:
- Smalltalk:
- SNOBOL:
- Tcl:
- Vala:
- Verilog:
- VHDL:
- Visual Basic.NET: vbnet
- Visual FoxPro: foxpro
- XQuery:
- Zephir:
- Template Languages:
- Cheetah:
- Django:
- ERB:
- Genshi:
- Jinja:
- JSP:
- Mako:
- Myghty:
- Smarty:
- Tea:
- Configuration Markup:
- ApacheConf:
- INI-style: ini
- Lighttpd:
- Nginx:
- Other Markups:
- BBCode:
- CMake:
- CSS:
- Debian control file: control
- Diff:
- DTD:
- Gettext Catalogs: pot
- Gnuplot:
- Groff:
- HTML:
- HTTP Session: http
- IRC log (irssi-format): irc
- Makefile:
- MoinMoin/Trac wiki markup: moin
- MySQL:
- POV-Ray Scenes: pov
- Ragel:
- Redcode:
- ReST:
- SQL:
- PostgreSQL: psql
- SQLite:
- Squid Configuration: squid
- TeX:
- tcsh:
- VimScript: vim
- Windows Batch Script: bat
- XML:
- XSLT:
- YAML:

9
data/schema.sql Normal file
View File

@ -0,0 +1,9 @@
CREATE TABLE posts (
id TEXT PRIMARY KEY NOT NULL,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, /* Created timestamp */
expiration TIMESTAMP, /* Created timestamp */
language TEXT NOT NULL, /* Language (if detected) */
title TEXT, /* Title/Filename */
code BLOB NOT NULL, /* Code */
html BLOB /* HTML version of Code */
);

96
lib/App/WerePaste.pm Normal file
View File

@ -0,0 +1,96 @@
package App::WerePaste;
use strict;
use warnings;
use App::WerePaste::Util::PygmentsBridge qw/:all/;
use Dancer2;
use Dancer2::Plugin::DBIC qw/schema/;
use DateTime;
use Data::UUID;
my $lastexpunge = 0;
sub DeploySchema {
# need to find a way to handle this that doesn't shit errors everywhere
eval {schema->deploy};
}
sub DateTimeToQueryable {
my $dt = DateTime->now(time_zone => config->{tz});
$dt->add(@_) if scalar @_;
return schema->storage->datetime_parser->format_datetime($dt);
}
sub ExpirationToDate {
my $expire = shift;
$expire = $expire ? { split ':', $expire } : undef;
return undef if $expire and $expire->{never};
return DateTimeToQueryable(%{ $expire || config->{default_expire} });
}
sub GetUUID {
my $uuid = Data::UUID->new->create_str;
$uuid =~ s/\-//g;
return lc $uuid;
}
sub CheckExpired {
return unless time > ($lastexpunge+900); #expunge once every 15 mins
$lastexpunge = time;
schema->resultset('Paste')->search({ expiration => [undef, { '<' => DateTimeToQueryable() }]})->delete_all;
}
sub ValidateParams {
my $params = shift;
if($params->{id}) {
return undef unless lc($params->{id}) =~ /^[a-f0-9]*$/;
return 1;
}
return undef unless $params->{code};
return undef unless $params->{title} =~ /^[a-zA-Z0-9\.\-_ @\(\)]{0,255}$/;
return undef unless $params->{lang} =~ /^[a-z0-9\.\-\+# ]{0,40}$/;
return undef unless $params->{expiration} =~ /^([a-z]+:[0-9]+)(,[a-z]+:[0-9]+)*$/ or not $params->{expiration};
return 1;
}
sub GetPaste {
my $id = shift;
return schema->resultset('Paste')->single({ id => $id }) or return undef;
}
sub SubmitPaste {
my $params = shift;
my ($lang,$html) = PygmentsHighlight(lang => $params->{lang}, code => $params->{code});
my $id = GetUUID();
my $result = schema->resultset('Paste')->create({
id => $id,
title => $params->{title},
language => $lang,
expiration => ExpirationToDate($params->{expiration}),
code => $params->{code},
html => $html,
}) or return undef;
return $id;
}
sub ValidateAndGet {
my $params = shift;
ValidateParams($params) or return undef;
return GetPaste(lc $params->{id}) or return undef;
}
# Startup
DeploySchema();
# Hooks
hook 'before' => sub { CheckExpired(); };
# Routes
#get
get '/' => sub { template 'index.tt'; };
get '/:id' => sub { my $paste=ValidateAndGet(scalar params('route')) or pass; template 'show.tt', { paste => $paste };};
get '/:id/copy' => sub { my $paste=ValidateAndGet(scalar params('route')) or pass; template 'index.tt', { paste => $paste }; };
get '/:id/raw' => sub { my $paste=ValidateAndGet(scalar params('route')) or pass; content_type 'text/plain'; return $paste->code; };
#post
post '/' => sub {
my $p = params('body');
ValidateParams($p) or return send_error('Submitted paste is not valid. Check your post title and language, and try again.',400);
my $id = SubmitPaste($p) or return redirect '/503.html';
return redirect "/$id";
};
#default
any qr/.*/ => sub { return send_error('Page gone',404); };
1;
__END__

View File

@ -0,0 +1,11 @@
package App::WerePaste::Schema;
use strict;
use warnings;
use base 'DBIx::Class::Schema';
__PACKAGE__->load_namespaces;
1;
__END__

View File

@ -0,0 +1,28 @@
package App::WerePaste::Schema::Result::Paste;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("pastes");
__PACKAGE__->add_columns(
'id', { data_type => 'text', is_nullable => 0 },
'ts', { data_type => 'timestamp', default_value => \"current_timestamp", is_nullable => 0 },
'expiration', { data_type => 'timestamp', is_nullable => 1 },
'language', { data_type => 'text', is_nullable => 0 },
'title', { data_type => 'text', is_nullable => 1 },
'code', { data_type => 'blob', is_nullable => 0 },
'html', { data_type => 'blob', is_nullable => 1 },
);
__PACKAGE__->set_primary_key('id');
use Dancer2;
__PACKAGE__->load_components(qw/InflateColumn::DateTime/);
__PACKAGE__->add_columns(
'+ts' => { timezone => config->{tz}, locale => config->{locale} },
'+expiration' => { timezone => config->{tz}, locale => config->{locale}, formatter => 'DateTime::Formatter::MySQL' }
);
1;
__END__

View File

@ -0,0 +1,52 @@
package App::WerePaste::Util::PygmentsBridge;
use strict;
use Exporter;
use vars qw/$VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS/;
use Carp;
use Encode qw/decode_utf8/;
use Try::Tiny;
use Inline Python => <<'END_INLINE_PYTHON';
from pygments import highlight
from pygments.lexers import guess_lexer, get_lexer_by_name
from pygments.formatters import HtmlFormatter
def py_highlight(c,l):
return highlight(c,l,HtmlFormatter(linenos=True,anchorlinenos=True,lineanchors="L",linenospecial=10,linespans="L",encoding="utf-8"))
def py_guesslexer(c):
return guess_lexer(c,encoding="utf-8")
def py_getlexer(l):
return get_lexer_by_name(l,encoding="utf-8")
def py_getnamefromlexer(l):
return l.name
END_INLINE_PYTHON
$VERSION = 1.0.0;
@ISA = qw/Exporter/;
@EXPORT = qw//;
@EXPORT_OK = qw/PygmentsHighlight/;
%EXPORT_TAGS = (all => [@EXPORT_OK]);
sub PygmentsHighlight {
my (%params) = @_;
return unless $params{code};
my $lexer;
try {
$lexer = py_getlexer($params{lang}) if $params{lang};
} catch {};
try {
$lexer = py_guesslexer($params{code}) unless $lexer;
} catch {};
$lexer = py_getlexer('text') unless $lexer;
return (py_getnamefromlexer($lexer),decode_utf8(py_highlight($params{code},$lexer)));
}
1;
__END__

26
public/reset.css Normal file
View File

@ -0,0 +1,26 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}

87
public/solarized256.css Normal file
View File

@ -0,0 +1,87 @@
/* Solarized Dark
For use with Jekyll and Pygments
http://ethanschoonover.com/solarized
SOLARIZED HEX ROLE
--------- -------- ------------------------------------------
base03 #002b36 background
base01 #586e75 comments / secondary content
base1 #93a1a1 body text / default code / primary content
orange #cb4b16 constants
red #dc322f regex, special keywords
blue #268bd2 reserved keywords
cyan #2aa198 strings, numbers
green #859900 operators, other keywords
*/
.paste { background-color: #002b36; color: #93a1a1 }
.paste .c { color: #586e75 } /* Comment */
.paste .err { color: #93a1a1 } /* Error */
.paste .g { color: #93a1a1 } /* Generic */
.paste .k { color: #859900 } /* Keyword */
.paste .l { color: #93a1a1 } /* Literal */
.paste .n { color: #93a1a1 } /* Name */
.paste .o { color: #859900 } /* Operator */
.paste .x { color: #cb4b16 } /* Other */
.paste .p { color: #93a1a1 } /* Punctuation */
.paste .cm { color: #586e75 } /* Comment.Multiline */
.paste .cp { color: #859900 } /* Comment.Preproc */
.paste .c1 { color: #586e75 } /* Comment.Single */
.paste .cs { color: #859900 } /* Comment.Special */
.paste .gd { color: #2aa198 } /* Generic.Deleted */
.paste .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */
.paste .gr { color: #dc322f } /* Generic.Error */
.paste .gh { color: #cb4b16 } /* Generic.Heading */
.paste .gi { color: #859900 } /* Generic.Inserted */
.paste .go { color: #93a1a1 } /* Generic.Output */
.paste .gp { color: #93a1a1 } /* Generic.Prompt */
.paste .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */
.paste .gu { color: #cb4b16 } /* Generic.Subheading */
.paste .gt { color: #93a1a1 } /* Generic.Traceback */
.paste .kc { color: #cb4b16 } /* Keyword.Constant */
.paste .kd { color: #268bd2 } /* Keyword.Declaration */
.paste .kn { color: #859900 } /* Keyword.Namespace */
.paste .kp { color: #859900 } /* Keyword.Pseudo */
.paste .kr { color: #268bd2 } /* Keyword.Reserved */
.paste .kt { color: #dc322f } /* Keyword.Type */
.paste .ld { color: #93a1a1 } /* Literal.Date */
.paste .m { color: #2aa198 } /* Literal.Number */
.paste .s { color: #2aa198 } /* Literal.String */
.paste .na { color: #93a1a1 } /* Name.Attribute */
.paste .nb { color: #B58900 } /* Name.Builtin */
.paste .nc { color: #268bd2 } /* Name.Class */
.paste .no { color: #cb4b16 } /* Name.Constant */
.paste .nd { color: #268bd2 } /* Name.Decorator */
.paste .ni { color: #cb4b16 } /* Name.Entity */
.paste .ne { color: #cb4b16 } /* Name.Exception */
.paste .nf { color: #268bd2 } /* Name.Function */
.paste .nl { color: #93a1a1 } /* Name.Label */
.paste .nn { color: #93a1a1 } /* Name.Namespace */
.paste .nx { color: #93a1a1 } /* Name.Other */
.paste .py { color: #93a1a1 } /* Name.Property */
.paste .nt { color: #268bd2 } /* Name.Tag */
.paste .nv { color: #268bd2 } /* Name.Variable */
.paste .ow { color: #859900 } /* Operator.Word */
.paste .w { color: #93a1a1 } /* Text.Whitespace */
.paste .mf { color: #2aa198 } /* Literal.Number.Float */
.paste .mh { color: #2aa198 } /* Literal.Number.Hex */
.paste .mi { color: #2aa198 } /* Literal.Number.Integer */
.paste .mo { color: #2aa198 } /* Literal.Number.Oct */
.paste .sb { color: #586e75 } /* Literal.String.Backtick */
.paste .sc { color: #2aa198 } /* Literal.String.Char */
.paste .sd { color: #93a1a1 } /* Literal.String.Doc */
.paste .s2 { color: #2aa198 } /* Literal.String.Double */
.paste .se { color: #cb4b16 } /* Literal.String.Escape */
.paste .sh { color: #93a1a1 } /* Literal.String.Heredoc */
.paste .si { color: #2aa198 } /* Literal.String.Interpol */
.paste .sx { color: #2aa198 } /* Literal.String.Other */
.paste .sr { color: #dc322f } /* Literal.String.Regex */
.paste .s1 { color: #2aa198 } /* Literal.String.Single */
.paste .ss { color: #2aa198 } /* Literal.String.Symbol */
.paste .bp { color: #268bd2 } /* Name.Builtin.Pseudo */
.paste .vc { color: #268bd2 } /* Name.Variable.Class */
.paste .vg { color: #268bd2 } /* Name.Variable.Global */
.paste .vi { color: #268bd2 } /* Name.Variable.Instance */
.paste .il { color: #2aa198 } /* Literal.Number.Integer.Long */

62
public/style.css Normal file
View File

@ -0,0 +1,62 @@
@charset "UTF-8";
body {
font-family: 'lucida console', monospace;
font-size: 11px;
background:#002b36;
color: #586e75;
}
textarea {
font-size: 12px;
font-family: 'lucida console', monospace;
padding: 0;
border: none;
border-top: 1px solid #335e69;
border-bottom: 1px solid #335e69;
background:#113c47;
color: #93a1a1;
width: 100%;
}
h1 {
font-size: 1.4em;
}
a, a:hover, a:visited, a:focus {
text-decoration: none;
color: #ccf;
}
.highlighttable {
font-size: 12px;
line-height: 14px;
width: 100%;
border-spacing: 0;
}
td {
border: 2px solid #335e69;
padding: 2px 0;
}
td.code {
width: 100%;
border-right: none;
border-left: 1px solid #335e69;
}
td.linenos {
border-left: none;
border-right: 1px solid #335e69;
}
td.linenos > .linenodiv > pre > a {
display: inline-block;
padding-top: 2px;
}
.highlight > pre > span {
display: block;
width: 100%;
padding-top: 2px;
}
.highlight > pre > span:hover {
background-color: #113c47;
}
.selected {
background-color: #335e69;
}

5
start Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env sh
[ -z "$APP" ] && exit 1
cd /home/sites/$APP
pkill -F /home/sites/$APP.pid 2>/dev/null
plackup -s Starman -D -p 12080 ./bin/app.pl --pid /home/sites/$APP.pid

30
views/index.tt Normal file
View File

@ -0,0 +1,30 @@
<form action="/" method="POST">
<input type="text" name="title" placeholder="Title (optional)" value="[% paste.title %]" size="40" maxlength="255" />
<select class="language" name="lang" style="float: right">
<option value="" [% 'selected' UNLESS paste.language %]>Language: Auto</option>
<option value="text" [% 'selected' IF paste.language == 'text' %]>Text</option>
[% FOR group = settings.languages %]
<optgroup label="[% group.keys.0 %]">
[% FOR lang = group.values.0 %]
[% val = (lang.values.0 || lang.keys.0) | lower %]
<option value="[% val | lower %]" [% 'selected' IF paste.language == "${val}" %]>[% lang.keys.0 %]</option>
[% END %]
</optgroup>
[% END %]
</select>
<br />
<textarea name="code" rows="32">[% paste.code %]</textarea>
<br />
<div style='float: right'>
<select class="expiration" name="expiration">
<option value="hours:1" >Expire 1 hour from now</option>
<option value="hours:6" >Expire 6 hours from now</option>
<option value="days:1" >Expire 1 day from now</option>
<option value="weeks:1" >Expire 1 week from now</option>
<option value="months:1">Expire 1 month from now</option>
<option value="years:1" >Expire 1 year from now</option>
<option value="never:1" selected>Never expire</option>
</select>
<button type="submit">Save</button>
</div>
</form>

19
views/layouts/main.tt Normal file
View File

@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<title>[% IF paste %][% paste.title || paste.id %] - [% END %]paste.were.space</title>
<link rel="stylesheet" href="/reset.css" type="text/css"/>
<link rel="stylesheet" href="/style.css" type="text/css"/>
<link rel="stylesheet" href="/solarized256.css" type="text/css"/>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
</head>
<body>
<div id='nav'>
<a href="/">new paste</a>[% IF paste %] | <a href="/[% paste.id %]/copy">duplicate paste</a> | <a href="/[% paste.id %]/raw">raw paste data</a>[% END %] | v4: <span id='addr'></span> | v6: <span id='addr6'></span><br />
<span id='blurb'><a href="https://github.com/MaffC/WerePaste">App::WerePaste</a>. Abuse of this service will result in the innocent losing out.</span>
</div>
<br />
[% content %]
<script type='text/javascript'>var x4=x6=null;function ip(v){var x=new XMLHttpRequest();x.onreadystatechange=rt;x.open('GET','http://ip'+v+'.maff.scot/',true);x.send(null);v==6? x6=x : x4=x;};function rt(){if(x4!=null&&x4.status===200){document.getElementById('addr').textContent=x4.responseText;};if(x6!=null&&x6.status===200){document.getElementById('addr6').textContent=x6.responseText;}};ip(4);ip(6);</script>
</body>
</html>

27
views/show.tt Normal file
View File

@ -0,0 +1,27 @@
<h1>[% paste.title %]</h1>
<span id='dates'>Pasted at <b>[% paste.ts.hms %], [% paste.ts.dmy %]</b>
[% IF paste.expiration %] | Expires at <b>[% paste.expiration.hms %], [% paste.expiration.dmy %]</b>[% END %] | Language: [% paste.language %]</span>
<br />
<br />
<div class="paste">
[% IF paste.html %]
[% paste.html %]
[% ELSE %]
<pre>[% paste.code %]</pre>
[% END %]
</div>
<script type="text/javascript">
function highlight_line() {
var lnum = window.location.hash.match(/L-\d+/);
if(!lnum) { return; }
$('.linenos a').removeClass('selected');
$('.highlight span').removeClass('selected');
$('.linenos a[href="#'+lnum+'"]').addClass('selected');
$('.highlight span[id="'+lnum+'"]').addClass('selected');
}
$(document).ready(function() {
window.onhashchange = highlight_line;
if(location.hash.indexOf("#L")===0){highlight_line();}
});
</script>