Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 751 lines (632 sloc) 24.6 KB
#!/usr/bin/perl
#
# mPaste
# A lean, mean and clean, pastebinnin' machine.
#
# @author: firnsy
#
# mPaste 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.
#
# mPaste 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 General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with mPaste. If not, see <http://www.gnu.org/licenses/>.
#
#
# PERL INCLUDES
use DBI;
use Digest::MD5 qw(md5_base64);
use File::Path qw(mkpath);
use File::Spec;
use List::MoreUtils;
use Mojo::File;
use Mojolicious::Lite;
use Mojo::Util qw(decode encode);
use POSIX qw(floor);
use Time::Piece;
use constant {
CONTENT_TEXT => [qw(text/plain text/x-log application/x-desktop)],
CONTENT_IMAGE => [qw(image/gif image/jpg image/jpeg image/png)],
CONTENT_OBJECT => [qw()],
HYPNOTOAD_LISTEN => [split(/, /, $ENV{MPASTE_HYPNOTOAD_LISTEN} // 'http://*:8000')],
SECRETS => [split(/, /, $ENV{MPASTE_SECRETS} // 'mpaste')],
LOCK_FILE => $ENV{MPASTE_LOCK_FILE} // '/var/lock/subsys/mpaste.lock',
PID_FILE => $ENV{MPASTE_PID_FILE} // '/var/run/mpaste.pid',
PASTE_DIR => $ENV{MPASTE_PASTE_DIR} // '/tmp/mpaste',
REAP_INTERVAL => $ENV{MPASTE_REAP_INTERVAL} // 300, # defaults to reaping every 5 minutes
UPLOAD_LIMIT => $ENV{MPASTE_UPLOAD_LIMIT} // 10485760, # defaults to 10MB
DEFAULT_HOLD => $ENV{MPASTE_HOLD_TIME} // 7 * 24 * 3600, # defaults to 30 days
HIDE_HISTORY => !!$ENV{MPASTE_NO_HISTORY} // 0, # defaults to showing history
CUSTOM_TITLE => $ENV{MPASTE_CUSTOM_TITLE} // 'mPaste',
CUSTOM_CSS => $ENV{MPASTE_CUSTOM_CSS},
VERSION => '3.2',
};
#
# PRIVATE FUNCTIONS
sub _db() {
return DBI->connect('dbi:SQLite:dbname=' . PASTE_DIR . '/mpaste.db', '', '');
};
sub _build_db() {
return !!_db->do("CREATE TABLE IF NOT EXISTS paste (id INTEGER PRIMARY KEY AUTOINCREMENT, uuid CHAR(16) NOT NULL, title CHAR(128) NOT NULL DEFAULT '', name CHAR(32) NOT NULL DEFAULT '', path CHAR(512) NOT NULL, type INTEGER NOT NULL, hold INTEGER NOT NULL DEFAULT 604800, updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)");
}
#
# HELPERS
helper 'hide_history' => sub { return HIDE_HISTORY; };
helper 'paste_dump' => sub {
my $sth = _db->prepare('SELECT uuid, title, name, updated, hold, (strftime("%s","now") - strftime("%s",updated)) AS held, (datetime(updated,"+"||hold||" seconds")) AS expired, strftime("%s", (datetime(updated,"+"||hold||" seconds"))) AS expired_seconds FROM paste ORDER BY updated DESC');
my $pastes = !!($sth->execute) ? $sth->fetchall_arrayref({}) : [];
$sth->finish;
return $pastes;
};
helper 'paste_delete' => sub {
return 0 unless defined $_[1];
my $sth = _db->prepare('DELETE FROM paste WHERE uuid=?');
return !!($sth->execute($_[1]))
};
helper 'paste_get' => sub {
return undef unless defined $_[1];
my $sth = _db->prepare('SELECT type, path, title, name, hold, updated, (strftime("%s", updated)) AS updated_seconds FROM paste WHERE uuid=?');
my $paste = !!($sth->execute($_[1])) ? $sth->fetchrow_hashref : undef;
$sth->finish;
return $paste;
};
helper 'paste_set' => sub {
return 0 unless ref $_[1] eq 'HASH';
my $sth = _db->prepare('INSERT INTO paste (uuid, type, path, title, name, hold, updated) VALUES (?,?,?,?,?,?, CURRENT_TIMESTAMP)');
return !!($sth->execute($_[1]->{uuid}, $_[1]->{type}, $_[1]->{path}, $_[1]->{title}, $_[1]->{name}, $_[1]->{hold}));
};
helper 'paste_is_image' => sub {
my $type = ref $_[1] eq 'HASH' ? $_[1]->{type} // '' : '';
return !!grep { $_ eq $type } @{CONTENT_IMAGE()};
};
helper 'paste_is_object' => sub {
my $type = ref $_[1] eq 'HASH' ? $_[1]->{type} // '' : '';
return !!grep { $_ eq $type } @{CONTENT_OBJECT()};
};
helper 'paste_is_text' => sub {
my $type = ref $_[1] eq 'HASH' ? $_[1]->{type} // '' : '';
return !!grep { $_ eq $type } @{CONTENT_TEXT()};
};
helper 'time_distance_in_words' => sub {
my ($self, $time_from, $time_to) = (shift, shift, shift);
$time_from //= gmtime;
$time_to //= gmtime;
$time_from = Time::Piece->strptime($time_from, '%Y-%m-%d %H:%M:%S') unless ref($time_from) eq 'Time::Piece';
$time_to = Time::Piece->strptime($time_to, '%Y-%m-%d %H:%M:%S') unless ref($time_to) eq 'Time::Piece';
return 'not sure how long' unless (ref $time_from eq 'Time::Piece') && (ref $time_to eq 'Time::Piece');
my $distance = $time_to - $time_from;
my $distance_in_seconds = floor($distance->seconds);
if (List::MoreUtils::any { $distance_in_seconds == $_ } (0..4)) {
return 'less than 5 seconds';
}
elsif (List::MoreUtils::any {$distance_in_seconds == $_} (5..9)) {
return 'less than 10 seconds';
}
elsif (List::MoreUtils::any {$distance_in_seconds == $_} (10..19)) {
return 'less than 20 seconds';
}
elsif (List::MoreUtils::any {$distance_in_seconds == $_} (20..39)) {
return 'half a minute';
}
elsif (List::MoreUtils::any {$distance_in_seconds == $_} (40..59) ) {
return 'less than a minute';
}
elsif (List::MoreUtils::any {$distance_in_seconds == $_} (60..119) ) {
return 'about a minute';
}
my $distance_in_minutes = floor($distance->minutes);
if (List::MoreUtils::any {$distance_in_minutes == $_} (2..44) ) {
return sprintf('%d minute%s', $distance_in_minutes, $distance_in_minutes != 1 ? 's' : '');
}
elsif (List::MoreUtils::any {$distance_in_minutes == $_} (45..89)) {
return 'about an hour';
}
elsif (List::MoreUtils::any {$distance_in_minutes == $_} (90..1439)) {
my $distance_in_hours = floor($distance_in_minutes / 60);
return sprintf('about %d hour%s', $distance_in_hours, $distance_in_hours != 1 ? 's' : '');
}
elsif (List::MoreUtils::any {$distance_in_minutes == $_} (1440..2519)) {
return 'about a day';
}
elsif (List::MoreUtils::any {$distance_in_minutes == $_} (2520..43199)) {
my $distance_in_days = floor($distance_in_minutes / 1440);
return sprintf('%d day%s', $distance_in_days, $distance_in_days != 1 ? 's' : '');
}
elsif (List::MoreUtils::any {$distance_in_minutes == $_} (43200..86399)) {
return 'about one month';
}
elsif (List::MoreUtils::any {$distance_in_minutes == $_} (86400..525599)) {
my $distance_in_months = floor($distance_in_minutes / 43200);
return sprintf('about %d month%s', $distance_in_months, $distance_in_months != 1 ? 's' : '');
}
my $distance_in_years = floor($distance->years);
if (List::MoreUtils::any {$distance_in_years == $_} (0..1)) {
return 'about a year';
}
return sprintf('about %d year%s', $distance_in_years, $distance_in_years != 1 ? 's' : '');
};
#
# PASTE REAPER
Mojo::IOLoop->recurring(REAP_INTERVAL() => sub {
my $self = shift;
my $db = _db;
my $sth = $db->prepare('SELECT uuid, title, name, updated, hold, (strftime("%s","now") - strftime("%s",updated)) AS held, (datetime(updated,"+"||hold||" seconds")) AS expired FROM paste WHERE (hold-held) <= 0 ORDER BY updated DESC');
my $res = $sth->execute();
my $pastes = !!$res ? $sth->fetchall_arrayref({}) : [];
$sth->finish;
if(@{$pastes}) {
app->log->info('Reaping old pastes.');
my @delete_uuids;
foreach my $p (@$pastes) {
my $path = Mojo::File->new(PASTE_DIR, $p->{uuid});
app->log->info(sprintf 'Deleting %s - %s (%s)', $path, $p->{title}, $p->{name});
unlink $path->to_string and push @delete_uuids, $p->{uuid};
}
$res = !!$db->do('DELETE FROM paste WHERE uuid IN ("' . join('","', @delete_uuids) . '")') if @delete_uuids;
}
else {
app->log->info('No pastes to reap.');
}
return $res;
});
#
# ROUTES
get '/' => sub {
my $c = shift;
my $data = $c->flash('data') // {
title => '',
name => $c->cookie('name') // '',
hold => 0,
};
$c->stash(
custom_css => CUSTOM_CSS,
paste => $data,
title => CUSTOM_TITLE,
version => VERSION,
);
$c->render('index');
};
get '/history' => sub {
my $c = shift;
# abort early if no history is configured
return $c->redirect_to('/') if HIDE_HISTORY;
$c->stash(
custom_css => CUSTOM_CSS,
pastes => $c->paste_dump,
title => CUSTOM_TITLE,
version => VERSION
);
$c->render('history');
}, 'history_view';
get '/paste/:uuid' => sub {
my $c = shift;
my $uuid = $c->param('uuid');
my $paste = $c->paste_get($uuid);
unless($paste) {
$c->paste_delete($uuid);
$c->flash(error => 'Paste does not exist.');
return $c->redirect_to('/');
}
my $path = Mojo::File->new($paste->{path});
unless(-r $path->to_string) {
$c->paste_delete($uuid);
$c->flash(error => 'Paste does not exist.');
return $c->redirect_to('/');
}
$paste->{data} = decode "UTF-8", $path->slurp if $c->paste_is_text($paste);
$c->stash(
created => $c->flash('created') // 0,
custom_css => CUSTOM_CSS,
paste => $paste,
title => CUSTOM_TITLE,
version => VERSION,
);
$c->render('paste');
}, 'paste_view';
get '/paste/:uuid/raw' => sub {
my $c = shift;
my $paste = $c->paste_get($c->param('uuid'));
my $file = Mojo::Asset::File->new(path => $paste->{path});
$paste->{title} =~ s/[^A-Za-z0-9_-]//g;
$paste->{type} .= ';charset=utf-8' if $paste->{type} eq 'text/plain';
$c->res->headers->content_type($paste->{type} . ';name=' . $paste->{title});
$c->res->headers->content_disposition('inline;filename=' . $paste->{title});
$c->res->content->asset($file);
$c->rendered(200);
}, 'paste_raw';
post '/paste' => sub {
my $c = shift;
my $paste = {
hold => $c->param('hold') // DEFAULT_HOLD,
name => $c->param('name') // $c->cookie('name'),
title => $c->param('title'),
};
$c->cookie(name => $paste->{name});
do {
$paste->{uuid} = substr md5_base64(time.rand), 0, 11;
$paste->{uuid} =~ tr|/+|qQ|;
$paste->{path} = Mojo::File->new(PASTE_DIR, $paste->{uuid});
} while -e $paste->{path};
my $file = $c->req->upload('file');
if ($file && length $file->filename) {
if ($file->size > UPLOAD_LIMIT) {
$c->flash(error => 'Upload exceeded ' . UPLOAD_LIMIT . 'bytes in size.');
return $c->redirect_to('/');
}
$paste->{title} //= $file->filename;
$paste->{type} //= $file->headers->content_type;
$file = $file->move_to($paste->{path});
}
else {
my $data = $c->param('data');
unless(length $data) {
$c->flash(error => 'Nothing to paste.');
return $c->redirect_to('/');
}
$paste->{type} //= 'text/plain';
$paste->{path}->spurt(encode("UTF-8", $data));
}
$paste->{title} = 'No Title' unless length $paste->{title};
$paste->{name} = 'Anonymous' unless length $paste->{name};
unless($c->paste_set($paste)) {
$c->flash(error => 'Unable to create paste.');
$c->redirect_to('/');
}
# rebuild reaper
# flash we're a new paste and redirect to it
$c->flash(created => 1);
return $c->redirect_to('paste_view', uuid => $paste->{uuid});
}, 'paste_save';
get '/#trap' => [trap => ''] => sub {
my $c = shift;
$c->flash(error => 'Nothing to see at: ' . $c->stash('trap'));
$c->redirect_to('/');
};
#
# START
# create store path as requried
-d PASTE_DIR or Mojo::File->new(PASTE_DIR)->make_path or app->log->fatal("Can't create paste directory ($!)") and die;
_build_db;
# production configuration
app->config(hypnotoad => {
listen => HYPNOTOAD_LISTEN,
lock_file => LOCK_FILE,
pid_file => PID_FILE
});
# log some startup params
app->log->info('Using paste dir: ' . PASTE_DIR);
app->log->info('History: ' . (HIDE_HISTORY ? 'disabled' : 'enabled'));
app->log->info('Upload limit: ' . UPLOAD_LIMIT . ' bytes');
app->log->info('Hold time: ' . DEFAULT_HOLD . ' seconds');
app->log->info('Custom CSS: ' . CUSTOM_CSS) if CUSTOM_CSS;
app->secrets(SECRETS);
app->start;
__DATA__
@@ layouts/main.html.ep
<html>
<head>
<title><%= $title %></title>
<link rel="stylesheet" href="/css/font-awesome-4.4.0.min.css" type="text/css">
<link rel="stylesheet" href="/css/mpaste.css" type="text/css">
% if (my $custom_css = stash('custom_css')) {
<link rel="stylesheet" href="<%= $custom_css %>" type="text/css">
% }
<script src="/js/jquery-3.2.1.min.js"></script>
<script>
Number.prototype.between = function(a, b, inclusive) {
var min = Math.min(a, b),
max = Math.max(a, b);
return inclusive ? this >= min && this <= max : this > min && this < max;
}
var time_distance_in_words = function(time_from, time_to) {
var d = new Date()
var now = Math.floor(d.getTime() / 1000);
time_from = time_from || now;
time_to = time_to || now;
if (time_from === undefined || time_to === undefined) {
return 'not sure how long';
}
var distance_in_seconds = Math.floor(time_to - time_from);
if (distance_in_seconds.between(0, 4, true)) {
return 'less than 5 seconds';
}
else if (distance_in_seconds.between(5, 9, true)) {
return 'less than 10 seconds';
}
else if (distance_in_seconds.between(10, 19, true)) {
return 'less than 20 seconds';
}
else if (distance_in_seconds.between(20, 39, true)) {
return 'half a minute';
}
else if (distance_in_seconds.between(40, 59, true)) {
return 'less than a minute';
}
else if (distance_in_seconds.between(60, 119, true)) {
return 'about a minute';
}
var distance_in_minutes = Math.floor(distance_in_seconds / 60);
if (distance_in_minutes.between(2, 44, true)) {
return distance_in_minutes + ' minute' + (distance_in_minutes != 1 ? 's' : '');
}
else if (distance_in_minutes.between(45, 89, true)) {
return 'about an hour';
}
else if (distance_in_minutes.between(90, 1439, true)) {
var distance_in_hours = Math.floor(distance_in_minutes / 60);
return 'about ' + distance_in_hours + ' hour' + (distance_in_hours != 1 ? 's' : '');
}
else if (distance_in_minutes.between(1440, 2519, true)) {
return 'about a day';
}
else if (distance_in_minutes.between(2520, 43199, true)) {
var distance_in_days = Math.floor(distance_in_minutes / 1440);
return distance_in_days + ' day' + (distance_in_days != 1 ? 's' : '');
}
else if (distance_in_minutes.between(43200, 86399, true)) {
return 'about one month';
}
else if (distance_in_minutes.between(86400, 525599, true)) {
var distance_in_months = Math.floor(distance_in_minutes / 43200);
return 'about ' + distance_in_months + ' month' + (distance_in_months != 1 ? 's' : '');
}
var distance_in_years = Math.floor(distance_in_seconds / 31540000);
if (distance_in_years.between(0, 1, true)) {
return 'about a year';
}
return 'about ' + distance_in_years + ' year' + (distance_in_years != 1 ? 's' : '');
};
</script>
</head>
<body>
% if (defined flash('error')) {
<!-- NOTIFICATIONS -->
<div id="page-notifications">
<div class="alert">
<button class="alert-btn-close"><i class="fa fa-times" aria-hidden="true"></i></button>
<%= flash('error') %>
</div>
</div>
% }
<div class="content">
<%= content %>
</div>
<!-- HELP MODAL -->
<div class="modal-wrapper">
<div class="modal">
<div class="modal-header">
<button class="modal-btn-close"><i class="fa fa-times" aria-hidden="true"></i></button>
<h1>Help</h1>
</div>
<div class="modal-content">
<p><%= $title ne 'mPaste' ? "$title (built on mPaste)" : 'mPaste' %> is a clean, mean and lean pastebinnin' machine.</p>
<h4>Viewing Pastes</h4>
<p>Syntax highlighting is enabled by default. When viewing "source code" pastes, you can force language hinting by adding a <code>?pretty=</code> value, for example:</p>
<ul>
<li><code>?pretty=lisp</code> or</li>
<li><code>?pretty=r</code></li>
</ul>
<p>You can disable highlighting with <code>?pretty=none</code>.</p>
<p>Selecting the RAW button (<i class="fa fa-fw fa-file"></i>) will render the paste in its original content-type.</p>
</div>
</div>
</div>
<script>
$(document).ready(function() {
$('#show-help').on('click', function() {
$('body').addClass('modal-open');
});
$('.alert-btn-close').on('click', function() {
$(this).parent().fadeOut();
});
$('.modal-btn-close').on('click', function() {
$('body').removeClass('modal-open');
});
});
</script>
</body>
</html>
@@ index.html.ep
% layout 'main';
<header>
<div id="paste-toolbar" class="toolbar">
<a href="#" id="paste-file" class="btn btn-default" data-tooltip-position="left" data-tooltip="Add file"><i class="fa fa-fw fa-file-o"></i></a>
% if(!hide_history) {
<a href="<%= url_for('history_view') %>" class="btn btn-default" data-tooltip-position="left" data-tooltip="History"><i class="fa fa-fw fa-history"></i></a>
<button id="show-help" class="btn btn-default" data-tooltip-position="left" data-tooltip="Help"><i class="fa fa-fw fa-question"></i></a>
% }
</div>
<h1><a href="#"><%= $title %></a></h1>
</header>
<main>
<div id="paste-content" class="paste-data">
<div class="paste-modal">
<i class="fa fa-3x fa-file"></i>
<h2 id="paste-object-name"></h2>
<h3 id="paste-object-size"></h3>
<button id="paste-file-remove" class="btn btn-default" data-tooltip="Remove" data-tooltip-position="bottom"><i class="fa fa-fw fa-times"></i></button>
</div>
<textarea form="paste-form" id="paste-raw" name="data" autofocus><%= $paste->{data} %></textarea>
</div>
</main>
<footer>
<div class="pull-right">
<p>v<%= $version %> - by <a href="https://firnsy.com" target="blank">firnsy</a></p>
</div>
<form id="paste-form" action="<%= url_for('paste_save') %>" method="post" name="pasteform" enctype="multipart/form-data">
<button type="submit" class="control btn btn-default" data-tooltip-position="right" data-tooltip="Paste"><i class="fa fa-fw fa-paste"></i></button>
<input type="text" class="control" name="title" value="<%= $paste->{title} %>" maxlength="128" placeholder="No Title"/>
<input type="text" class="control" name="name" value="<%= $paste->{name} %>" maxlength="32" placeholder="Anonymous" />
<input id="file-input" type="file" name="file" class="hidden"/>
</form>
</footer>
<script>
$(document).ready(function() {
var file = undefined;
var bytesToHuman = function(a,b,c,d,e) {
return (b=Math,c=b.log,d=1024,e=c(a)/c(d)|0,a/b.pow(d,e)).toFixed(2)+' '+(e?'KMGTPEZY'[--e]+'iB':'Bytes');
}
var refreshHandler = function() {
var have_text = $('#paste-raw').val().length > 0;
var have_file = typeof(file) === 'object';
var file_view = $('.paste-modal');
var text_view = $('#paste-raw');
if (have_file) {
text_view.fadeOut();
file_view.fadeIn();
}
else if (file_view.is(':visible')) {
file_view.fadeOut();
text_view.fadeIn().focus();
}
text_view.prop('disable', have_file)
$('#paste-file').toggleClass('active', have_file);
$('#paste-text').toggleClass('active', !have_file && have_text);
};
$('#paste-raw').on('keyup', refreshHandler);
var addFileHandler = function(files) {
if ((length in files) && (files.length > 0)) {
file = files[0];
$('#paste-object-name').text(file.name);
$('#paste-object-size').text(bytesToHuman(file.size));
}
refreshHandler();
};
var noopEventHandler = function(e) { e.stopPropagation(); e.preventDefault() };
var removeFileHandler = function() {
file = undefined;
refreshHandler();
};
$('#paste-file-remove').on('click', removeFileHandler);
$('#file-input').on('change', function(e) {
addFileHandler( e.target.files );
});
$('#paste-file')
.on('click', function(e) {
e.stopPropagation(); e.preventDefault();
$('#file-input').click();
});
$('#paste-raw')
.on('dragenter', function() {
$(this).addClass('dragging');
})
.on('dragover', noopEventHandler)
.on('dragleave', function(e) {
$(this).removeClass('dragging');
e.stopPropagation(); e.preventDefault();
refreshHandler();
})
.on('drop', function(e) {
$(this).removeClass('dragging');
e.stopPropagation(); e.preventDefault();
addFileHandler( e.originalEvent.dataTransfer.files );
});
});
</script>
@@ history.html.ep
% layout 'main';
<header>
<div id="paste-toolbar" class="toolbar">
<a href="<%= url_for("/") %>" class="btn btn-default" data-tooltip-position="left" data-tooltip="New"><i class="fa fa-fw fa-plus"></i></a>
<button id="show-help" class="btn btn-default" data-tooltip-position="left" data-tooltip="Help"><i class="fa fa-fw fa-question"></i></a>
</div>
<h1><a href="#"><%= $title %></a></h1>
</header>
<main>
<div class="history-list">
<h1>Paste History</h1>
<ul>
% for my $p (@{$pastes}) {
<li>
<a href="<%= url_for('paste_view', uuid => $p->{uuid}) %>"><h2><%= $p->{title} %></h2></a>
<p>Pasted by <%= $p->{name} %>, expires in <span class="time_expires" data-expires="<%= $p->{expired_seconds} %>"><%= time_distance_in_words(undef, $p->{expired}) %></span>.</p>
</li>
% }
</ul>
</div>
</main>
<footer>
<div class="pull-right">
<p>v<%= $version %> - by <a href="https://firnsy.com" target="blank">firnsy</a></p>
</div>
</footer>
<script>
$('document').ready(function() {
setInterval(function() {
$('.time_expires').each(function() {
var self = $(this);
self.html(time_distance_in_words(undefined, self.data('expires')));
});
}, 2000);
});
</script>
@@ paste.html.ep
% layout 'main';
<header>
<div id="paste-toolbar" class="toolbar">
<a href="<%= url_for("/") %>" class="btn btn-default" data-tooltip-position="left" data-tooltip="New"><i class="fa fa-fw fa-plus"></i></a>
<a href="<%= url_for('paste_raw', uuid => $uuid) %>" class="btn btn-default" data-tooltip-position="left" data-tooltip="Raw"><i class="fa fa-fw fa-file"></i></a>
% if(paste_is_text($paste)) {
<button id="clipboard" class="btn btn-default" data-clipboard-target="#paste-raw" data-tooltip-position="left" data-tooltip="Copy"><i class="fa fa-fw fa-copy"></i></button>
% }
% if(!hide_history) {
<a href="<%= url_for('history_view') %>" class="btn btn-default" data-tooltip-position="left" data-tooltip="History"><i class="fa fa-fw fa-history"></i></a>
% }
<button id="show-help" class="btn btn-default" data-tooltip-position="left" data-tooltip="Help"><i class="fa fa-fw fa-question"></i></a>
</div>
<h1 title="<%= $paste->{title} %>"><a href="<%= url_for('current') %>"><%= $paste->{title} %></a></h1>
</header>
<main>
<div id="paste-content" class="paste-data">
% if(paste_is_text($paste)) {
<pre><code id="paste-raw" class="<%= (param 'pretty') ? (param 'pretty') : '' %>"><%= $paste->{data} %></code></pre>
% } elsif(paste_is_image($paste)) {
<div id="paste-image-raw">
<img src="<%= url_for('paste_raw', uuid => $uuid) %>"/>
</div>
% } elsif(paste_is_object($paste)) {
<div id="paste-object-raw">
<iframe src="<%= url_for('paste_raw', uuid => $uuid) %>" type="<%= $paste->{type} %>" frameborder="0"></iframe>
</div>
% } else {
<div id="paste-other-raw">
<p>We can't inline view for pastes of MIME type "<%= $paste->{type} %>".</p>
<p>Try viewing/downloading via <a href="<%= url_for('paste_raw', uuid => $uuid) %>" class="btn btn-default"><i class="fa fa-fw fa-file"></i> Raw</a></p>
</div>
% }
</div>
</main>
<footer>
<h1>Pasted by <%= $paste->{name} %>, <span id="time_updated"><%= time_distance_in_words($paste->{updated}) %></span> ago.</h1>
<div class="pull-right">
<p>v<%= $version %> - by <a href="https://firnsy.com" target="blank">firnsy</a></p>
</div>
</footer>
<script src="/js/clipboard-1.6.1.min.js"></script>
<script src="/js/highlight-9.11.0.full.min.js"></script>
<script>
$('document').ready(function() {
timeUpdatedLabel = $('#time_updated');
setInterval(function() {
timeUpdatedLabel.html(time_distance_in_words(<%= $paste->{updated_seconds} %>));
}, 2000);
% if(paste_is_text($paste)) {
var clipboard = new Clipboard('#clipboard');
var clipboardBtn = $('#clipboard');
clipboard.on('success', function(e) {
clipboardBtn.attr('data-tooltip', 'Copied!')
setTimeout(function() { clipboardBtn.attr('data-tooltip', 'Copy'); }, 2000);
e.clearSelection();
});
% }
});
% if (param('pretty') ne 'none') {
hljs.initHighlightingOnLoad();
% }
</script>