master
Sven Slootweg 9 years ago
parent 242c5596fd
commit 80ddb08e0b

11
.gitignore vendored

@ -1,3 +1,12 @@
# https://git-scm.com/docs/gitignore # https://git-scm.com/docs/gitignore
# https://help.github.com/articles/ignoring-files # https://help.github.com/articles/ignoring-files
# Example .gitignore files: https://github.com/github/gitignore # Example .gitignore files: https://github.com/github/gitignore
/storage/
/node_modules/
/config.json
/persist/
/errors/
/TODO
/thumbnails/
/logs/
.sass-cache

@ -0,0 +1,263 @@
express = require('express')
uuid = require "uuid"
fs = require "fs"
domain = require "domain"
app = express()
reportError = (err, type = "error", sync = false) ->
errorPayload = {}
Object.getOwnPropertyNames(err).forEach (key) ->
errorPayload[key] = err[key]
filename = "errors/#{type}-#{Date.now()}-#{uuid.v4()}.json"
if sync
fs.writeFileSync filename, JSON.stringify(errorPayload)
else
fs.writeFile filename, JSON.stringify(errorPayload)
# To make absolutely sure that we can log and exit on all uncaught errors.
if app.get("env") == "production"
logAndExit = (err) ->
reportError(err, "error", true)
process.exit(1)
rootDomain = domain.create()
rootDomain.on "error", logAndExit
runWrapper = (func) ->
rootDomain.run ->
try
func()
catch err
logAndExit(err)
else
# In development, we don't have to catch uncaught errors. Just run the specified function directly.
runWrapper = (func) -> func()
runWrapper ->
path = require('path')
favicon = require('serve-favicon')
logger = require('morgan')
cookieParser = require('cookie-parser')
bodyParser = require('body-parser')
fancyshelf = require "fancyshelf"
session = require "express-session"
csurf = require "csurf"
fileStreamRotator = require "file-stream-rotator"
domainMiddleware = require("express-domain-middleware")
errors = require "errors"
PrettyError = require "pretty-error"
Convert = require "ansi-to-html"
marked = require "marked"
moment = require "moment"
persist = require "./lib/persist"
useCsrf = require "./lib/use-csrf"
templateUtil = require("./lib/template-util")
sessionStore = require("./lib/persist-session")(session)
config = require "./config.json"
knexfile = require("./knexfile").development
ansiHTML = new Convert(escapeXML: true, newline: true)
# Error handling
pe = PrettyError.start()
pe.skipPackage "bluebird", "coffee-script", "express", "express-promise-router", "jade"
pe.skipNodeFiles()
errors.create
name: "UploadError"
errors.create
name: "UploadTooLarge"
parents: errors.UploadError
status: 413
errors.create
name: "InvalidFiletype"
parents: errors.UploadError
status: 415
errors.create
name: "NotAuthenticated"
status: 403
errors.create
name: "InvalidInput"
status: 422
# Database setup
shelf = fancyshelf
engine: knexfile.client
host: knexfile.connection.host
username: knexfile.connection.user
password: knexfile.connection.password
database: knexfile.connection.database
debug: (app.get("env") == "development")
# Task runner
TaskRunner = require("./lib/task-runner")
runner = new TaskRunner(app: app, db: shelf, config: config, thumbnailPath: path.join(__dirname, 'thumbnails'))
runner.addTask "mirror", require("./tasks/mirror"), maxConcurrency: 5
runner.addTask "thumbnail", require("./tasks/thumbnail")
runner.run()
runner.on "taskQueued", (taskType, task) ->
persist.increment "task:#{taskType}:queued"
runner.on "taskStarted", (taskType, task) ->
persist.decrement "task:#{taskType}:queued"
persist.increment "task:#{taskType}:running"
runner.on "taskFailed", (taskType, task) ->
persist.decrement "task:#{taskType}:running"
persist.increment "task:#{taskType}:failed"
runner.on "taskCompleted", (taskType, task) ->
persist.decrement "task:#{taskType}:running"
persist.increment "task:#{taskType}:completed"
if app.get("env") == "development"
runner.on "taskFailed", (taskType, task, err) ->
console.log err.stack
else
runner.on "taskFailed", (taskType, task, err) ->
reportError err, "taskFailed"
# Configure Express
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'jade')
# Middleware
if app.get("env") == "development"
app.use(logger('dev'))
else
accessLogStream = fileStreamRotator.getStream frequency: (config.accessLog.frequency ? "24h"), filename: config.accessLog.filename
app.use logger (config.accessLog.format ? "combined"), stream: accessLogStream
# Using /static/ paths to maintain backwards compatibility.
app.use("/static/thumbs", express.static(path.join(__dirname, 'thumbnails')))
app.use("/static/pdfjs", express.static(path.join(__dirname, 'public/pdfjs')))
app.use(express.static(path.join(__dirname, 'public')))
#app.use(favicon(__dirname + '/public/favicon.ico'))
app.use domainMiddleware
app.use templateUtil
app.use shelf.express
app.use session
store: new sessionStore
persist: persist
secret: config.session.signingKey
resave: true # TODO: Implement `touch` for the session store, and/or switch to a different store.
saveUninitialized: false
# Load models
require("./models")(shelf)
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
app.use (req, res, next) ->
res.locals.md = marked
req.appConfig = config
req.persist = persist
jadeExports = [
"announcementVisible"
"announcementText"
"announcementLinkText"
"announcementLink"
"maintenanceMode"
"maintenanceModeText"
"donationGoal"
"donationTotal"
"showNotice"
]
for key in jadeExports
res.locals[key] = persist.getItem "var:#{key}"
res.locals.bitcoinAddress = config.donations.bitcoinAddress
res.locals.isAdmin = req.session.isAdmin
# This is for logging/reporting errors that should not occur, but that are known to not cause any adverse side-effects.
# This should NOT be used for errors that could leave the application in an undefined state!
req.reportError = reportError
req.taskRunner = runner
next()
app.use "/", require("./routes/index")
app.use "/donate", require("./routes/donate")
app.use "/d", require("./routes/document")
app.use "/upload", require("./routes/upload")
app.use "/gallery", require("./routes/gallery")
app.use "/blog", require("./routes/blog")
app.use "/admin", csurf(), useCsrf, require("./routes/admin")
# If no routes match, cause a 404
app.use (req, res, next) ->
next new errors.Http404Error("The requested page was not found.")
# Error handling middleware
app.use "/static/thumbs", (err, req, res, next) ->
# TODO: For some reason, Chrome doesn't always display images that are sent with a 404 status code. Let's stick with 200 for now...
#res.status 404
if err.status == '404'
fs.createReadStream path.join(__dirname, "public/images/no-thumbnail.png")
.pipe res
else
next(err)
app.use (err, req, res, next) ->
statusCode = err.status || 500
res.status(statusCode)
# Dump the error to disk if it's a 500, so that the error reporter can deal with it.
if app.get("env") != "development" and statusCode == 500
reportError(err)
# Display the error to the user - amount of details depending on whether the application is running in production mode or not.
if (app.get('env') == 'development')
stack = err
else
if statusCode == 500
errorMessage = "An unknown error occurred."
stack = {stack: "An administrator has been notified, and the error will be resolved as soon as possible. Apologies for the inconvenience."}
else
errorMessage = err.message
stack = {stack: err.explanation, subMessage: err.subMessage}
if req.headers["X-Requested-With"] == "XMLHttpRequest"
res.send {
message: errorMessage,
error: stack
}
else
if err instanceof errors.NotAuthenticated
res.redirect "/admin/login"
else
if app.get("env") == "development"
htmlStack = ansiHTML.toHtml(stack.stack)
.replace /#555/g, "#b5b5b5"
# TODO: It seems aborted responses will result in an attempt to send an error page/header - of course this can't succeed, as the headers have already been sent by that time, and an error is thrown.
res.render('error', {
message: errorMessage,
error: stack,
statusCode: statusCode,
htmlStack: htmlStack
})
module.exports = app

@ -0,0 +1,56 @@
#!/usr/bin/env coffee
app = require('../app')
debug = require('debug')('pdfy:server')
http = require('http')
normalizePort = (val) ->
port = parseInt val, 10
if isNaN port
return val
if port >= 0
return port
return false
onError = (error) ->
if error.syscall != "listen"
throw error
bind = if typeof port == "string"
"Pipe #{port}"
else
"Port #{port}"
switch error.code
when "EACCES"
console.error "#{bind} requires elevated privileges"
process.exit 1
when "EADDRINUSE"
console.error "#{bind} is already in use"
process.exit 1
else
throw error
onListening = ->
addr = server.address()
bind = if typeof port == "string"
"pipe #{port}"
else
"port #{port}"
debug("Listening on #{bind}")
port = normalizePort(process.env.PORT || '3000')
app.set('port', port)
server = http.createServer(app)
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)

@ -0,0 +1,51 @@
chokidar = require "chokidar"
nodemailer = require "nodemailer"
path = require "path"
fs = require "fs"
watcher = chokidar.watch "./errors", depth: 0, ignoreInitial: true
mailer = nodemailer.createTransport()
processFile = (filePath) ->
fs.readFile filePath, (err, data) ->
try
parsedData = JSON.parse(data)
catch error
console.log "Error report not complete yet, retrying #{filePath} in 1 second..."
setTimeout (->
processFile(filePath)
), 1000
return
errorMessage = parsedData?.message ? "UNKNOWN ERROR"
textStack = parsedData?.stack?.replace(/\u001b(?:\[\??(?:\d\;*)*[A-HJKSTfminsulh])?/g, "") ? ""
message = """
A failure occurred. #{filePath} is attached.
#{textStack}
"""
htmlMessage = """
A failure occurred. #{filePath} is attached.
<pre>#{textStack}</pre>
""".replace(/\n/g, "<br>")
mailer.sendMail
from: "ops@pdf.yt"
to: "admin@cryto.net"
subject: "Automatic failure report: #{errorMessage}"
text: message
html: htmlMessage
attachments: [
filename: path.basename(filePath)
path: filePath
contentType: "application/json"
]
watcher.on "add", (filePath) ->
console.log "PANIC! Sending report:", filePath
processFile(filePath)
console.log "Running..."

@ -0,0 +1,35 @@
require "./lib/upload"
require "./lib/embed"
require "./lib/donate"
require "./lib/form-popup"
$ = require "jquery"
autosize = require "autosize"
marked = require "marked"
scrollFloat = require "./lib/scroll-float"
$ ->
$(".checkAll").on "change", (event) ->
newValue = $(this).prop("checked")
$(this)
.closest "table"
.find "input[type='checkbox']"
.filter ->
return !$(this).hasClass "checkAll"
.prop "checked", newValue
scrollFloat($(".floating"))
autosize($(".md-editor"))
updatePreview = ->
$(".md-preview").html(marked($(this).val()))
$(".md-editor")
.on "change input propertychange", updatePreview
.each updatePreview

@ -0,0 +1,199 @@
$ = require "jquery"
require "Base64"
$ ->
selectedMethod = undefined
selectedAmount = undefined
selectedHasPrice = undefined
pulsateRemove = undefined
paypalHandler = (block) ->
block.find "input.amount"
.val (Math.round(selectedAmount * 100) / 100)
paymentMethodHandlers =
paypal: paypalHandler
"paypal-weekly": paypalHandler
"paypal-monthly": paypalHandler
bitcoin: (block) ->
block.find(".loading-message").show()
block.find(".loaded-content").hide()
$.get "/donate/convert/btc?amount=#{selectedAmount}", (btcAmount) ->
$.get "/donate/bip21?amount=#{btcAmount}", (uri) ->
$(".bip21").attr("href", uri)
block.find(".loading-message").hide()
block.find(".loaded-content").show()
$(".bip21-qr").attr("src", "/donate/bip21/qr?amount=#{btcAmount}")
$(".btc-amount").text(Math.round(btcAmount * 100000000) / 100000000)
setCustomValue = (value) ->
value = parseFloat(value)
if isNaN value
# TODO: Validation error!
value = 0
selectedAmount = value
$ "#custom_amount_input"
.on "keyup", (event) ->
setCustomValue $(this).val()
showInstructions()
.on "change", (event) ->
setCustomValue $(this).val()
showInstructions()
.on "click", (event) ->
#$ this
# .closest ".option"
# .find "input[type='radio']"
# .click()
#event.stopPropagation()
$ "#amount_custom"
.on "click", (event) ->
setCustomValue $("#custom_amount_input").val()
$ ".donation-page section.types .option input[type='radio']"
.on "click", (event) ->
# Hide any instructions that were already visible.
# TODO: Automatically show instructions again when switching back...?
$ ".donation-page section.instructions div"
.hide()
$ ".donation-page section.instructions .placeholder"
.show()
type = $ this
.closest ".option"
.data "type"
$ ".donation-page section.methods .option"
.hide()
$ ".donation-page section.methods .option[data-type='#{type}']"
.show()
$ ".donation-page section.methods .option input[type='radio']"
.on "click", (event) ->
optionElement = $ this
.closest ".option"
selectedMethod = method = optionElement.data "name"
selectedHasPrice = setPrice = !!(optionElement.data "set-price")
if setPrice
$ ".donation-page section.amount"
.slideDown(400)
instructionSection = $ ".donation-page section.instructions"
instructionSection
.children "h3.set-amount"
.show()
instructionSection
.children "h3.no-set-amount"
.hide()
else
$ ".donation-page section.amount"
.slideUp(400)
instructionSection = $ ".donation-page section.instructions"
instructionSection
.children "h3.set-amount"
.hide()
instructionSection
.children "h3.no-set-amount"
.show()
showInstructions()
$ ".donation-page section.amount .option input[type='radio']"
.on "click", (event) ->
if not ($(this).attr("id") == "amount_custom")
selectedAmount = $(this).val()
showInstructions()
showInstructions = ->
$ ".donation-page section.instructions .method"
.hide()
if selectedMethod? and (selectedAmount? or !selectedHasPrice)
$ ".donation-page section.instructions .placeholder"
.hide()
$ ".donation-page section.instructions .method-#{selectedMethod}"
.show()
#$ "html, body"
# .animate scrollTop: "#{$('.donation-page section.instructions').offset().top}px", 500
$ ".donation-page section.instructions"
.addClass "pulsate"
if pulsateRemove?
clearTimeout pulsateRemove
pulsateRemove = setTimeout (->
$ ".donation-page section.instructions"
.removeClass "pulsate"
), 3000
if selectedMethod of paymentMethodHandlers
paymentMethodHandlers[selectedMethod]($(".donation-page section.instructions .method-#{selectedMethod}"))
else
$ ".donation-page section.instructions .placeholder"
.show()
$ ".donation-page .option input[type='radio']"
.on "click", (event) ->
$ this
.closest "section"
.find ".option"
.removeClass "selected"
$ this
.closest ".option"
.addClass "selected"
$ this
.closest ".option"
.find "input[type='number']"
.focus()
event.stopPropagation()
$ ".donation-page .option"
.on "click", (event) ->
$ this
.find "input[type='radio']"
.click()
$ ".donation-page section.types .option[data-type='once']"
.click()
$ ".donation-page #amount_10"
.click()
$ ".donation-page section.instructions .method, .donation-page section.instructions h3.no-set-amount"
.hide()
$ ".js-unavailable"
.hide()
$ ".js-available"
.show()
$ ".decodable"
.each ->
$(this).html atob($(this).html())

@ -0,0 +1,33 @@
$ = require "jquery"
castBoolean = (value) ->
if value == true
return 1
else
return 0
updateEmbedCode = ->
showToolbar = $("#show_toolbar").prop("checked")
showDonationLink = $("#show_donation").prop("checked")
embedCode = embed_template
.replace "{SPARSE}", castBoolean(not showToolbar)
.replace "{DONATION}", castBoolean(showDonationLink)
$(".embed_code").val embedCode
$ ->
$ ".autoselect"
.on "click", (event) ->
$ this
.focus()
.select()
$ "#show_toolbar, #show_donation"
.on "change", (event) ->
updateEmbedCode()
# Linkify has a tendency of breaking our embed codes, so we re-set the embed code here to make sure that that doesn't happen.
if embed_template?
updateEmbedCode()
# do things and stuff

@ -0,0 +1,16 @@
$ = require "jquery"
$ ->
$(".popup").hide()
$(".form-popup").each ->
elem = $(this)
target = ".popup-#{elem.data('popup')}"
elem.on "click", ->
$(target).show()
$(".popup .close").on "click", ->
$(this)
.closest ".popup"
.hide()

@ -0,0 +1,49 @@
$ = require "jquery"
module.exports = (jqueryObj) ->
jqueryObj.each ->
elem = $(this)
minTop = threshold = basePos = undefined
currentlyFloating = false
originalPositioning = elem.css "position"
originalLeft = elem.css "left"
originalTop = elem.css "top"
originalLeft = elem.css "right"
updateMetrics = ->
needRestore = currentlyFloating
if needRestore
makeNotFloating()
basePos = elem.offset()
minTop = elem.data("min-top") ? 0
threshold = basePos.top - minTop
if needRestore
makeFloating()
makeFloating = ->
currentlyFloating = true
elem.css
position: "fixed"
top: "#{minTop}px"
right: "auto"
left: "#{basePos.left}px"
makeNotFloating = ->
currentlyFloating = false
elem.attr "style", ""
#elem.css
# position: originalPositioning
# top: originalTop
updateMetrics()
$(window).on "resize", updateMetrics
$(document).on "scroll", (event) ->
if $(document).scrollTop() >= threshold
makeFloating()
else
makeNotFloating()

@ -0,0 +1,176 @@
$ = require "jquery"
prettyUnits = require "pretty-units"
# The AMD loader for this package doesn't work for some reason - so we explicitly disable it. This will force it to fall back to the CommonJS API.
require "imports?define=>false!blueimp-file-upload"
data_object = null
uploadDone = (response) ->
switch response.status
when 415
errorHeader = "Oops! That's not a PDF file."
errorMessage = "The file you tried to upload is not a valid PDF file. Currently, only PDF files are accepted."
when 413
errorHeader = "Oops! That file is too big."
errorMessage = "The file you tried to upload is too big. Currently, you can only upload PDF files up to 150MB in size."
when 200 # Nothing, success!
else
errorHeader = "Oops! Something went wrong."
errorMessage = "An unknown error occurred. Please reload the page and try again. If the error keeps occurring, <a href='mailto:pdfy@cryto.net'>send us an e-mail</a>!"
if errorMessage?
triggerError errorHeader, errorMessage
reinitializeUploader()
else
if response.responseJSON.redirect?
window.location = response.responseJSON.redirect
else
# TODO: Wat do?
triggerError = (header, message) ->
$(".upload-form .privacySettings, .upload-form .progress, .upload-form .button-submit").hide()
$(".upload").removeClass("faded")
errorBox = $("#uploadError")
.show()
errorBox.find "h3"
.html header
errorBox.find ".message"
.html message
data_object = null
reinitializeUploader = ->
$("#upload_element").replaceWith($("#upload_element").clone(true))
filePicked = (data) ->
$(".upload-form .privacySettings, .upload-form .button-submit").show()
$("#uploadError").hide()
$(".upload-form .fileinfo").removeClass("faded")
fileinfo = $(".fileinfo")
filesize = data.files[0].size
# TODO: Use filesize limit from configuration file!
if filesize > (150 * 1024 * 1024)
reinitializeUploader()
triggerError("Oops! That file is too big.", "The file you tried to upload is too big. Currently, you can only upload PDF files up to 150MB in size.")
return
filesize_text = prettyUnits(filesize) + "B"
fileinfo.find ".filename"
.text data.files[0].name
fileinfo.find ".filesize"
.text filesize_text
$ ".info"
.hide()
fileinfo
.show()
$ ".upload"
.addClass "faded"
updateUploadProgress = (event) ->
if event.lengthComputable
percentage = event.loaded / event.total * 100
done_text = prettyUnits(event.loaded) + "B"
total_text = prettyUnits(event.total) + "B"
progress = $ ".progress"
progress.find ".done"
.text done_text
progress.find ".total"
.text total_text
progress.find ".percentage"
.text (Math.ceil(percentage * 100) / 100)
progress.find ".bar-inner"
.css width: "#{percentage}%"
if event.loaded >= event.total
# Completed!
progress.find ".numbers"
.hide()
progress.find ".wait"
.show()
$ ->
if $().fileupload?
# Only run this if the fileupload plugin is loaded; we don't need all this on eg. the 'view' page.
$ "#upload_form"
.fileupload
fileInput: null
type: "POST"
url: "/upload"
paramName: "file"
autoUpload: false
maxNumberOfFiles: 1
formData: (form) ->
form = $ "#upload_form"
form.serializeArray()
progressall: (e, data) ->
updateUploadProgress
lengthComputable: true
loaded: data.loaded
total: data.total
add: (e, data) ->
data_object = data
filePicked(data)
always: (e, data) ->
uploadDone(data.jqXHR)
$ "#upload_activator"
.on "click", (event) ->
$("#upload_element").click()
$ "#upload_element"
.on "change", (event) ->
filePicked(this)
$ "#upload_form"
.on "submit", (event) ->
event.stopPropagation()
event.preventDefault()
$ ".fileinfo"
.addClass "faded"
$ ".progress"
.show()
if data_object == null
# Only do this if the drag-and-drop dropzone hasn't been used.
formData = new FormData(this)
$.ajax
method: "POST"
url: "/upload"
data: formData
cache: false
contentType: false
processData: false
xhr: ->
customHandler = $.ajaxSettings.xhr()
if customHandler.upload?
customHandler.upload.addEventListener "progress", updateUploadProgress, false
return customHandler
complete: (result) ->
uploadDone(result)
else
# If the dropzone was used...
data_object.submit()

@ -0,0 +1,17 @@
#!/usr/bin/env coffee
scrypt = require "scrypt-for-humans"
Promise = require "bluebird"
read = Promise.promisify(require "read")
Promise.try ->
read(prompt: "Enter a password:", silent: true)
.spread (password, isDefault) ->
if password.trim().length == 0
console.log "You didn't enter a password!"
process.exit(1)
scrypt.hash(password)
.then (hash) ->
console.log "Hash:", hash
console.log "Set this hash in your config.json to use it."

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="12.000021"
height="13.574016"
viewBox="0 0 12.000021 13.574015"
enable-background="new 0 0 599.998 583.111"
xml:space="preserve"
id="svg3039"
inkscape:version="0.48.4 r9939"
sodipodi:docname="ia-icon.svg"><metadata
id="metadata3096"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3094" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1125"
id="namedview3092"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="7.9867624"
inkscape:cx="34.68682"
inkscape:cy="12.715741"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="A" /><g
id="A"
transform="translate(-140.28052,-561.46621)"><rect
x="140.28052"
y="574.42487"
width="12.000021"
height="0.61536318"
id="rect3058"
style="fill:#ffffff" /><rect
x="140.75385"
y="573.24133"
width="11.076934"
height="0.8520959"
id="rect3060"
style="fill:#ffffff" /><rect
x="140.68288"
y="563.53723"
width="11.017765"
height="1.1834365"
id="rect3062"
style="fill:#ffffff" /><polygon
points="347.701,162.012 539.506,162.012 551.193,149.072 347.701,101.486 144.21,149.072 155.897,162.012 "
id="polygon3064"
transform="matrix(0.02835123,0,0,0.02835123,136.33399,558.58896)"
style="fill:#ffffff" /><path
d="m 142.27205,568.4671 c -0.009,-0.58121 -0.0245,-1.16243 -0.0474,-1.74323 -0.0213,-0.54724 -0.0567,-1.09393 -0.0833,-1.64099 -9.2e-4,-0.0473 -0.0222,-0.0574 -0.0615,-0.066 -0.1614,-0.0351 -0.32366,-0.0517 -0.4866,-0.052 -0.16294,2.8e-4 -0.32517,0.0169 -0.48659,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0266,0.54706 -0.0618,1.09375 -0.0832,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0506,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0221,0.27872 0.18084,0.047 0.36017,0.0742 0.53937,0.0766 0.17922,-9.3e-4 0.35851,-0.0294 0.53938,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0222,-0.27872 0.027,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0287,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
id="path3084"
inkscape:connector-curvature="0"
style="fill:#ffffff" /><path
d="m 145.27192,568.4671 c -0.009,-0.58121 -0.0245,-1.16243 -0.0474,-1.74323 -0.0214,-0.54724 -0.0567,-1.09393 -0.0833,-1.64099 -9.2e-4,-0.0473 -0.0223,-0.0574 -0.0615,-0.066 -0.16139,-0.0351 -0.32366,-0.0517 -0.48656,-0.052 -0.16295,2.8e-4 -0.3252,0.0169 -0.4866,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0265,0.54706 -0.0618,1.09375 -0.0832,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0506,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0221,0.27872 0.18089,0.047 0.3602,0.0742 0.53938,0.0766 0.17921,-9.3e-4 0.35849,-0.0294 0.53939,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0222,-0.27872 0.0268,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0286,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
id="path3086"
inkscape:connector-curvature="0"
style="fill:#ffffff" /><path
d="m 148.75103,568.4671 c -0.009,-0.58121 -0.0245,-1.16243 -0.0473,-1.74323 -0.0214,-0.54724 -0.0566,-1.09393 -0.0832,-1.64099 -9.4e-4,-0.0473 -0.0223,-0.0574 -0.0616,-0.066 -0.16137,-0.0351 -0.32363,-0.0517 -0.48656,-0.052 -0.16293,2.8e-4 -0.32518,0.0169 -0.48661,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0265,0.54706 -0.0618,1.09375 -0.0833,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0505,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0222,0.27872 0.18088,0.047 0.36017,0.0742 0.53938,0.0766 0.17919,-9.3e-4 0.3585,-0.0294 0.53938,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0221,-0.27872 0.027,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0287,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
id="path3088"
inkscape:connector-curvature="0"
style="fill:#ffffff" /><path
d="m 151.67987,568.4671 c -0.009,-0.58121 -0.0246,-1.16243 -0.0474,-1.74323 -0.0214,-0.54724 -0.0567,-1.09393 -0.0833,-1.64099 -9.2e-4,-0.0473 -0.0222,-0.0574 -0.0615,-0.066 -0.16135,-0.0351 -0.32364,-0.0517 -0.48657,-0.052 -0.1629,2.8e-4 -0.32518,0.0169 -0.48655,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0266,0.54706 -0.0618,1.09375 -0.0833,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0506,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0221,0.27872 0.1809,0.047 0.3602,0.0742 0.53938,0.0766 0.17921,-9.3e-4 0.35852,-0.0294 0.53938,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0221,-0.27872 0.027,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0286,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
id="path3090"
inkscape:connector-curvature="0"
style="fill:#ffffff" /></g></svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="180"
height="261"
id="svg4889"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="no-thumbnail.svg">
<defs
id="defs4891" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="88.419794"
inkscape:cy="5.8296482"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1125"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata4894">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-791.36218)">
<g
id="g5420"
transform="translate(4.8995525,0)">
<path
inkscape:connector-curvature="0"
id="path5414"
d="m 32.763905,870.12363 0,98.58436 104.673085,0 0,-141.62912 -56.606425,0 -48.06666,43.04476 z"
style="fill:#f3f2f2;fill-opacity:1;stroke:#e3e3e3;stroke-width:1.14982712;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="rect4901"
d="m 80.830565,827.07887 -48.06666,43.04476 48.06666,0 0,-43.04476 z"
style="fill:#f3f2f2;fill-opacity:1;stroke:#e3e3e3;stroke-width:1.05590618;stroke-linejoin:bevel;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text5416"
y="949.12549"
x="85.945068"
style="font-size:78.12385559px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#e0e0e0;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
y="949.12549"
x="85.945068"
id="tspan5418"
sodipodi:role="line">?</tspan></text>
</g>
<text
xml:space="preserve"
style="font-size:31.59504318px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#b3b3b3;fill-opacity:1;stroke:none;font-family:Sans"
x="19.971004"
y="1014.6339"
id="text5426"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan5428"
x="19.971004"
y="1014.6339"
style="font-size:18.95702553px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#b3b3b3;font-family:Luxi Sans;-inkscape-font-specification:Luxi Sans">no thumbnail yet</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -0,0 +1,85 @@
var gulp = require('gulp');
/* CoffeeScript compile deps */
var path = require('path');
var gutil = require('gulp-util');
var concat = require('gulp-concat');
var rename = require('gulp-rename');
var coffee = require('gulp-coffee');
var cache = require('gulp-cached');
var remember = require('gulp-remember');
var plumber = require('gulp-plumber');
var livereload = require('gulp-livereload');
var nodemon = require("gulp-nodemon");
var net = require("net");
var webpack = require("gulp-webpack");
var sass = require("gulp-sass");
task = {
"source": ["public/**/*.coffee", "routes/**/*.coffee", "models/**/*.coffee", "tasks/**/*.coffee", "app.coffee", "util.coffee", "migrate.coffee", "db.coffee"]
}
/*
gulp.task('coffee', function() {
return gulp.src(task.source, {base: "."})
.pipe(plumber())
.pipe(cache("coffee"))
.pipe(coffee({bare: true}).on('error', gutil.log)).on('data', gutil.log)
.pipe(remember("coffee"))
.pipe(gulp.dest("."));
});*/
gulp.task('webpack', function(){
return gulp.src("frontend/index.coffee")
.pipe(webpack({
watch: true,
module: {
loaders: [{ test: /\.coffee$/, loader: "coffee-loader" }]
},
resolve: { extensions: ["", ".web.coffee", ".web.js", ".coffee", ".js"] }
}))
.pipe(rename("bundle.js"))
.pipe(gulp.dest("public/js/"));
});
gulp.task('sass', function(){
// TODO: Put the source SCSS file in a more logical place...
return gulp.src("./public/css/*.scss")
.pipe(sass())
.pipe(gulp.dest("./public/css"));
});
function checkServerUp(){
setTimeout(function(){
var sock = new net.Socket();
sock.setTimeout(50);
sock.on("connect", function(){
console.log("Trigger page reload...");
livereload.changed();
sock.destroy();
})
.on("timeout", checkServerUp)
.on("error", checkServerUp)
.connect(3000);
}, 70);
}
gulp.task('watch', function () {
livereload.listen();
gulp.watch(['./**/*.css', 'views/**/*.jade', 'package.json', "./public/js/**/*.js"]).on('change', livereload.changed);
gulp.watch(["./public/css/style.scss"], ["sass"])
//gulp.watch(task.source, ['coffee']);
// theseus disabled for now, it was screwing with my tracebacks
//nodemon({script: "./bin/www", ext: "js", nodeArgs: ['/usr/bin/node-theseus']}).on("start", checkServerUp);
nodemon({
script: "./bin/www.coffee",
ext: "coffee",
delay: 500,
ignore: ["./frontend/"],
watch: ["app.coffee", "bin", "lib", "models", "routes", "tasks"]
}).on("start", checkServerUp).on("restart", function(file){
console.log("Restarted triggered by:", file);
});
});
gulp.task('default', [/*'coffee',*/ 'sass', 'watch', 'webpack']);

@ -0,0 +1,17 @@
# Update with your config settings.
config = require "./config.json"
module.exports =
# TODO: Do we need an environment name here?
development:
client: "mysql2"
connection:
database: config.database.database
user: config.database.username
password: config.database.password
pool:
min: 2
max: 10
migrations:
tableName: "knex_migrations"

@ -0,0 +1,5 @@
module.exports = (req, res, next) ->
if (not res.locals.maintenanceMode) or req.session.isAdmin
next()
else
res.status(503).send(res.locals.maintenanceModeText)

@ -0,0 +1,25 @@
Promise = require "bluebird"
bhttp = require "bhttp"
lastRates = null
lastRateCheck = 0
module.exports = ->
Promise.try ->
if Date.now() > lastRateCheck + (5 * 60 * 1000)
# We need fresh API data, 5 minutes have elapsed.
Promise.try ->
Promise.all [
bhttp.get "http://api.fixer.io/latest", decodeJSON: true
bhttp.get "https://blockchain.info/ticker", decodeJSON: true
]
.spread (fixerRates, blockchainRates) ->
eurRates = fixerRates.body.rates
eurRates.BTC = 1 / blockchainRates.body.EUR["15m"]
Promise.resolve eurRates
.then (rates) ->
lastRates = rates
lastRateCheck = Date.now()
Promise.resolve rates
else
Promise.resolve lastRates

@ -0,0 +1,7 @@
errors = require "errors"
module.exports = (req, res, next) ->
if req.session?.isAdmin?
next()
else
next(new errors.NotAuthenticated("You are not logged in as an administrator."))

@ -0,0 +1,11 @@
errors = require "errors"
amountRegex = /^[0-9]+(?:\.[0-9]+)?$/
module.exports = (amount) ->
parsedAmount = parseFloat(amount)
if amountRegex.exec(amount) == null or isNaN(parsedAmount)
throw new errors.InvalidInput("The specified amount is invalid.")
return parsedAmount

@ -0,0 +1,5 @@
module.exports = (param) ->
if not param?
return undefined
else
return !!(parseInt(param))

@ -0,0 +1,71 @@
# NOTE: This module does not currently ensure correct writes. Callbacks are called immediately (but asynchronously).
AbstractClientStore = require "express-brute/lib/AbstractClientStore"
module.exports = class PersistBruteStore extends AbstractClientStore
constructor: (options) ->
@persist = options.persist
@prefix = options.prefix ? "brute"
@_timers = {}
@_keyMatcher = new RegExp("^#{@prefix}:")
@persist.keys()
.filter (key) => key.match(@_keyMatcher)
.forEach (key) =>
@_createExpiryTimer key, @persist.getItem(key).expiry
_createExpiryTimer: (key, expiry) ->
@_removeExpiryTimer(key)
ttl = expiry - Date.now()
if ttl < 0
@_createRemover(key)()
else
setTimeout @_createRemover(key), ttl
_removeExpiryTimer: (key) ->
if @_timers[key]?
clearTimeout @_timers[key]
delete @_timers[key]
_createRemover: (key) ->
return =>
@_removeExpiryTimer(key)
prefixedKey = "#{@prefix}:#{key}"
# TODO: Error handling?
@persist.removeItem prefixedKey
set: (key, value, lifetime, callback) ->
prefixedKey = "#{@prefix}:#{key}"
expiry = (Date.now() + lifetime)
@_createExpiryTimer key, expiry
@persist.setItem prefixedKey, {value: value, expiry: expiry}, callback
process.nextTick ->
callback(null)
get: (key, callback) ->
prefixedKey = "#{@prefix}:#{key}"
result = @persist.getItem(prefixedKey)
value = result?.value
# Normalize to dates if we're reading from disk-persisted data... on disk, they're saved as strings.
if value?.firstRequest? and value?.firstRequest not instanceof Date
value.firstRequest = new Date(value.firstRequest)
if value?.lastRequest? and value?.lastRequest not instanceof Date
value.lastRequest = new Date(value.lastRequest)
process.nextTick ->
callback(null, value)
reset: (key, callback) ->
# I don't really understand why this is called 'reset'. It's quite clearly a 'remove' function...
@_createRemover(key)()
process.nextTick ->
callback(null)

@ -0,0 +1,40 @@
# This will break horribly in a multi-process setup! Don't do that!
# NOTE: Does not currently ensure writes.
module.exports = (session) ->
class PersistSessionStore extends session.Store
constructor: (options) ->
@persist = options.persist
get: (sid, callback) ->
sessionData = @persist.getItem "session:#{sid}"
process.nextTick ->
callback(null, sessionData)
set: (sid, sessionData, callback) ->
sessionData.__lastAccess = Date.now()
@persist.setItem "session:#{sid}", sessionData
process.nextTick ->
callback(null)
destroy: (sid, session, callback) ->
@persist.removeItem "session:#{sid}"
process.nextTick ->
callback(null)
length: (callback) ->
length = @persist.valuesWithKeyMatch(/^session:/).length
process.nextTick ->
callback(null, length)
clear: (callback) ->
@persist.keys()
.filter (key) -> key.match(/^session:/)
.forEach (key) ->
@persist.removeItem key
process.nextTick ->
callback(null)
# TODO: .touch

@ -0,0 +1,74 @@
Promise = require "bluebird"
persist = require "node-persist"
path = require "path"
xtend = require "xtend"
# We MUST explicitly specify the `persist` directory, otherwise node-persist will bug out and write to its own module directory...
persist.initSync(continuous: false, dir: path.join(__dirname, "../persist"))
persist.increment = (key, amount = 1) ->
persist.setItem key, (persist.getItem(key) + amount)
persist.decrement = (key, amount = 1) ->
persist.setItem key, (persist.getItem(key) - amount)
persist.addListItem = (key, item) ->
newList = [item].concat (persist.getItem(key) ? [])
persist.setItem key, newList
persist.removeListItem = (key, item) ->
newList = (persist.getItem(key) ? [])
.filter (existingItem) ->
return (item == existingItem)
persist.setItem key, newList
persist.removeListItemByFilter = (key, filter) ->
newList = (persist.getItem(key) ? [])
.filter (item) ->
return !filter(item)
persist.setItem key, newList
persist.setProperty = (key, propertyKey, value) ->
newObj = {}
newObj[propertyKey] = value
oldObj = persist.get(key)
persist.setItem key, xtend(oldObj, newObj)
persist.removeProperty = (key, propertyKey) ->
# Extremely ghetto shallow clone
obj = xtend({}, persist.get(key))
delete obj[propertyKey]
persist.setItem key, obj
# Rough shim for write queueing...
writeQueue = []
currentlyWriting = false
_setItem = persist.setItem
persist.setItem = (key, value) ->
new Promise (resolve, reject) ->
_setItem.call(persist, key, value)
addItemToQueue key, value, resolve, reject
triggerWrite()
addItemToQueue = (key, value, resolveFunc, rejectFunc) ->
writeQueue.push [key, value, resolveFunc, rejectFunc]
triggerWrite = ->
if not currentlyWriting and writeQueue.length > 0
currentlyWriting = 1
[key, value, resolveFunc, rejectFunc] = writeQueue.shift()
Promise.resolve(persist.persistKey(key))
.then (result) -> resolveFunc(result)
.catch (err) -> rejectFunc(err)
.finally ->
currentlyWriting = false
triggerWrite()
module.exports = persist

@ -0,0 +1,15 @@
Promise = require "bluebird"
crypto = Promise.promisifyAll(require "crypto")
module.exports = (length = 16) ->
Promise.try ->
byteLength = Math.ceil(length / 4) * 3
return crypto.randomBytesAsync(byteLength)
.then (bytes) ->
bytes = bytes
.toString "base64"
.replace /\+/g, "-"
.replace /\//g, "_"
.slice 0, length
Promise.resolve bytes

@ -0,0 +1,34 @@
class RateLimiter
constructor: (@limit, @interval, @funcA, @funcB) ->
@_totalCalls = 0
@_startTimer()
_startTimer: ->
@_timer = setInterval @_clearCalls, @interval
_stopTimer: ->
if @_timer?
clearInterval @_timer
@_timer = null
_clearCalls: ->
@_totalCalls = 0
call: ->
@_totalCalls += 1
targetFunc = switch
when @_totalCalls <= @limit then @funcA
else @funcB
targetFunc.apply this, arguments
setInterval: (interval) ->
@_stopTimer()
@interval = interval
@_startTimer()
setLimit: (limit) ->
@limit = limit
module.exports = ->
return (funcA, funcB, options) ->
if not options.limit?
throw new Error("No limit specified.")
options.interval ?= 60 # Default: 60 seconds ie. 1 minute.
return new RateLimiter(options.limit, options.interval, funcA, funcB)

@ -0,0 +1,12 @@
Promise = require "bluebird"
concatStream = require "concat-stream"
buffertools = require "buffertools"
module.exports = (stream, needle) ->
# CAUTION: This buffers up in memory!
new Promise (resolve, reject) ->
stream
.pipe concatStream (result) ->
resolve buffertools.indexOf(result, needle) != -1
.on "error", (err) ->
reject err

@ -0,0 +1,13 @@
# NOTE: This is purely a `tap` equivalent for errors! Any resolves or rejections are ignored - promises can only be used to wait for async execution of something.
Promise = require "bluebird"
module.exports = (func) ->
return (err) ->
Promise.try ->
func(err)
.catch ->
# Consume any errors
Promise.resolve()
.then ->
Promise.reject(err)

@ -0,0 +1,95 @@
EventEmitter = require("events").EventEmitter
Promise = require "bluebird"
debug = require("debug")("task-runner")
makeExternalPromise = ->
extResolve = extReject = null
new Promise (resolve, reject) ->
extResolve = resolve
extReject = reject
module.exports = class TaskRunner extends EventEmitter
constructor: (@context = {}) ->
@_taskTypes = {}
@_queue = {}
@_runningCount = {}
@running = false
@context.taskRunner = this
_checkRunTask: ->
if not @running
return
debug "checking for runnable tasks..."
for taskType, options of @_taskTypes
if @_runningCount[taskType] < (options.maxConcurrent ? Infinity)
if options.maxConcurrent?
tasksToRun = options.maxConcurrent - @_runningCount[taskType]
else
tasksToRun = Infinity
debug "running #{tasksToRun} tasks"
for i in [0...tasksToRun]
if @_queue[taskType].length == 0
debug "ran out of tasks"
@emit "tasksDepleted"
break
@_doRunTask taskType, @_queue[taskType].shift()
debug "started #{tasksToRun} tasks, waiting for completion..."
_doRunTask: (taskType, taskData) ->
taskOptions = @_taskTypes[taskType]
@_runningCount[taskType] += 1
@emit "taskStarted", taskType, taskData.task
Promise.resolve(taskOptions.taskFunc(taskData.task, @context))
.then (value) =>
@_markTaskCompleted taskType, taskData
taskData.resolveFunc(value)
.catch (err) =>
@emit "taskFailed", taskType, taskData.task, err
taskData.rejectFunc(err)
debug "started task"
_markTaskCompleted: (taskType, task) ->
@_runningCount[taskType] -= 1
@emit "taskCompleted", taskType, task.task
debug "completed task"
@_checkRunTask()
addTask: (taskType, taskFunc, options = {}) =>
options.taskFunc = taskFunc
@_taskTypes[taskType] = options
@_queue[taskType] = []
@_runningCount[taskType] = 0
debug "added task"
setTaskOptions: (taskType, options) =>
options.taskFunc = @_taskTypes[taskType].taskFunc
@_taskTypes[taskType] = options
do: (taskType, task) =>
@emit "taskQueued", taskType, task
debug "queued task"
new Promise (resolve, reject) =>
@_queue[taskType].push
resolveFunc: resolve
rejectFunc: reject
task: task
@_checkRunTask()
run: =>
@running = true
debug "started task loop"
@_checkRunTask()
pause: =>
@running = false
debug "paused task loop"

@ -0,0 +1,15 @@
moment = require "moment"
module.exports = (req, res, next) ->
res.locals.conditionalClasses = (always, conditionals) ->
applicableConditionals = (className for className, condition of conditionals when condition)
applicableClasses = always.concat applicableConditionals
return applicableClasses.join " "
res.locals.makeBreakable = (string) ->
require("jade/lib/runtime").escape(string).replace(/_/g, "_<wbr>")
res.locals.shortDate = (date) ->
moment(date).format "MMM Do, YYYY hh:mm:ss"
next()

@ -0,0 +1,4 @@
module.exports = (req, res, next) ->
token = req.csrfToken()
res.locals.csrfToken = token
next()

@ -0,0 +1,8 @@
exports.up = (knex, Promise) ->
knex.schema.table "documents", (table) ->
table.boolean "CDN"
exports.down = (knex, Promise) ->
knex.schema.table "documents", (table) ->
table.dropColumn "CDN"

@ -0,0 +1,8 @@
exports.up = (knex, Promise) ->
knex.schema.table "documents", (table) ->
table.boolean "Thumbnailed"
exports.down = (knex, Promise) ->
knex.schema.table "documents", (table) ->
table.dropColumn "Thumbnailed"

@ -0,0 +1,8 @@
exports.up = (knex, Promise) ->
knex.schema.table "documents", (table) ->
knex.schema.raw "ALTER TABLE documents ADD INDEX (Public)"
exports.down = (knex, Promise) ->
#

@ -0,0 +1,13 @@
exports.up = (knex, Promise) ->
knex.schema.createTable "blog_posts", (table) ->
table.bigIncrements("Id")
table.string("Slug")
table.string("Title")
table.text("Body", "longtext")
table.timestamp("Posted").nullable()
table.timestamp("Edited").nullable()
exports.down = (knex, Promise) ->
knex.schema.dropTable "blog_posts"

@ -0,0 +1,61 @@
rfr = require "rfr"
persist = rfr "lib/persist"
initializeVariable = (name, type, initialValue) ->
Promise.all [
persist.addListItem "variableTypes",
name: name
type: type
persist.setItem "var:#{name}", initialValue
]
removeVariable = (name) ->
Promise.all [
persist.removeListItemByFilter "variableTypes", (item) ->
return (item.name == name)
persist.removeItem "var:#{name}"
]
initializeTaskType = (name) ->
Promise.all [
persist.addListItem "taskTypes", name
persist.setItem "task:#{name}:running", 0
persist.setItem "task:#{name}:queued", 0
persist.setItem "task:#{name}:failed", 0
]
removeTaskType = (name) ->
Promise.all [
persist.removeListItem "taskTypes", name
persist.removeItem "task:#{name}:running"
persist.removeItem "task:#{name}:queued"
persist.removeItem "task:#{name}:failed"
]
exports.up = (knex, Promise) ->
Promise.all [
initializeVariable "cdnRateLimit", "number", 0
initializeVariable "announcementText", "string", ""
initializeVariable "announcementLinkText", "string", ""
initializeVariable "announcementLink", "string", ""
initializeVariable "announcementVisible", "boolean", false
initializeVariable "maintenanceMode", "boolean", false
initializeVariable "maintenanceModeText", "text", ""
initializeTaskType "mirror"
initializeTaskType "thumbnail"
]
exports.down = (knex, Promise) ->
Promise.all [
removeVariable "cdnRateLimit"
removeVariable "announcementText"
removeVariable "announcementLinkText"
removeVariable "announcementLink"
removeVariable "announcementVisible"
removeVariable "maintenanceMode"
removeVariable "maintenanceModeText"
removeTaskType "mirror"
removeTaskType "thumbnail"
]

@ -0,0 +1,9 @@
exports.up = (knex, Promise) ->
knex.schema.table "documents", (table) ->
table.boolean "Disabled"
.defaultTo 0
exports.down = (knex, Promise) ->
knex.schema.table "documents", (table) ->
table.dropColumn "Disabled"

@ -0,0 +1,24 @@
rfr = require "rfr"
persist = rfr "lib/persist"
migrateTaskType = (name) ->
Promise.all [
persist.setItem "task:#{name}:completed", 0
]
rollbackTaskType = (name) ->
Promise.all [
persist.removeItem "task:#{name}:completed"
]
exports.up = (knex, Promise) ->
Promise.all [
migrateTaskType "mirror"
migrateTaskType "thumbnail"
]
exports.down = (knex, Promise) ->
Promise.all [
rollbackTaskType "mirror"
rollbackTaskType "thumbnail"
]

@ -0,0 +1,33 @@
rfr = require "rfr"
persist = rfr "lib/persist"
initializeVariable = (name, type, initialValue) ->
Promise.all [
persist.addListItem "variableTypes",
name: name
type: type
persist.setItem "var:#{name}", initialValue
]
removeVariable = (name) ->
Promise.all [
persist.removeListItemByFilter "variableTypes", (item) ->
return (item.name == name)
persist.removeItem "var:#{name}"
]
exports.up = (knex, Promise) ->
Promise.all [
initializeVariable "donationGoal", "number", 500
initializeVariable "donationTotal", "number", 0
initializeVariable "showNotice", "boolean", false
]
exports.down = (knex, Promise) ->
Promise.all [
removeVariable "donationGoal"
removeVariable "donationTotal"
removeVariable "showNotice"
]

@ -0,0 +1,9 @@
exports.up = (knex, Promise) ->
knex.schema.table "documents", (table) ->
table.string "DisabledReason"
.nullable()
exports.down = (knex, Promise) ->
knex.schema.table "documents", (table) ->
table.dropColumn "DisabledReason"

@ -0,0 +1,6 @@
Promise = require "bluebird"
module.exports = (shelf) ->
shelf.model "BlogPost",
tableName: "blog_posts"
idAttribute: "Id"

@ -0,0 +1,6 @@
Promise = require "bluebird"
module.exports = (shelf) ->
shelf.model "Document",
tableName: "documents"
idAttribute: "Id"

@ -0,0 +1,13 @@
Promise = require "bluebird"
glob = Promise.promisify(require "glob")
rfr = require "rfr"
# This file automatically loads all models.
module.exports = (shelf) ->
Promise.try ->
glob "models/**/*.coffee"
.then (items) ->
for item in items
if item != "models/index.coffee"
rfr(item)(shelf)

@ -0,0 +1,77 @@
{
"name": "pdfy",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"Base64": "^0.3.0",
"ansi-to-html": "^0.3.0",
"bhttp": "^1.0.3",
"bip21": "1.0.0",
"bluebird": "^2.9.3",
"body-parser": "~1.10.2",
"buffertools": "^2.1.2",
"chokidar": "^1.0.1",
"concat-stream": "^1.4.7",
"connect-busboy": "0.0.2",
"cookie-parser": "~1.3.3",
"csurf": "^1.8.0",
"debug": "~2.1.1",
"errors": "^0.2.0",
"express": "~4.11.1",
"express-brute": "^0.5.3",
"express-domain-middleware": "^0.1.0",
"express-promise-router": "0.0.7",
"express-session": "^1.11.1",
"file-stream-rotator": "joepie91/file-stream-rotator",
"glob": "^4.3.5",
"gm": "^1.17.0",
"ia-headers": "^1.0.0",
"jade": "~1.9.1",
"knex": "^0.7.6",
"lodash": "^3.3.1",
"marked": "^0.3.3",
"moment": "^2.9.0",
"morgan": "~1.5.1",
"mysql2": "^0.15.4",
"node-persist": "joepie91/node-persist",
"nodemailer": "^1.3.4",
"pretty-error": "^1.1.1",
"qr-image": "^3.1.0",
"read": "^1.0.5",
"rfr": "^1.2.2",
"scrypt-for-humans": "^1.0.1",
"serve-favicon": "~2.2.0",
"session-file-store": "0.0.3",
"slug": "^0.8.0",
"stream-length": "^1.0.2",
"uuid": "^2.0.1",
"xtend": "^4.0.0",
"zero-fill": "^2.1.0"
},
"devDependencies": {
"autosize": "^3.0.0",
"blueimp-file-upload": "^9.9.3",
"coffee-loader": "^0.7.2",
"coffee-script": "^1.9.1",
"gulp": "^3.8.10",
"gulp-cached": "~0.0.3",
"gulp-coffee": "~2.0.1",
"gulp-concat": "~2.2.0",
"gulp-jade": "^0.7.0",
"gulp-livereload": "~2.1.0",
"gulp-nodemon": "~1.0.4",
"gulp-notify": "^1.6.0",
"gulp-plumber": "~0.6.3",
"gulp-remember": "~0.2.0",
"gulp-rename": "~1.2.0",
"gulp-sass": "^1.3.3",
"gulp-util": "~2.2.17",
"gulp-webpack": "^1.3.0",
"imports-loader": "^0.6.3",
"jquery": "^2.1.3",
"pretty-units": "^0.1.0"
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,714 @@
@-webkit-keyframes pulsate {
from {
box-shadow: 0 0 0px #000000; }
40% {
box-shadow: 0 0 15px #005f52; }
60% {
box-shadow: 0 0 15px #005f52; }
to {
box-shadow: 0 0 0px #000000; } }
@-moz-keyframes pulsate {
from {
box-shadow: 0 0 0px #000000; }
40% {
box-shadow: 0 0 15px #005f52; }
60% {
box-shadow: 0 0 15px #005f52; }
to {
box-shadow: 0 0 0px #000000; } }
@-o-keyframes pulsate {
from {
box-shadow: 0 0 0px #000000; }
40% {
box-shadow: 0 0 15px #005f52; }
60% {
box-shadow: 0 0 15px #005f52; }
to {
box-shadow: 0 0 0px #000000; } }
@-ms-keyframes pulsate {
from {
box-shadow: 0 0 0px #000000; }
40% {
box-shadow: 0 0 15px #005f52; }
60% {
box-shadow: 0 0 15px #005f52; }
to {
box-shadow: 0 0 0px #000000; } }
.pulsate {
-webkit-animation-name: pulsate;
-webkit-animation-duration: 800ms;
-webkit-animation-iteration-count: 3;
-moz-animation-name: pulsate;
-moz-animation-duration: 800ms;
-moz-animation-iteration-count: 3;
-o-animation-name: pulsate;
-o-animation-duration: 800ms;
-o-animation-iteration-count: 3;
-ms-animation-name: pulsate;
-ms-animation-duration: 800ms;
-ms-animation-iteration-count: 3; }
.error-stack {
background-color: black;
padding: 16px; }
.side-margins {
margin-left: 64px;
margin-right: 64px; }
.pure-button-small {
padding: 6px 11px;
font-size: 15px; }
.clearfix:after {
content: "";
display: table;
clear: both; }
.return-button {
margin-top: -3px;
float: right; }
body {
background-color: #2D2D31;
color: white;
font-family: "PT Sans", sans-serif;
padding: 0px;
margin: 0px;
font-size: 16px; }
body.full-screen {
overflow: hidden; }
body a {
color: #94EBD9;
text-decoration: none; }
body a:hover {
/*color: #67C7B3;*/
color: #BAF5E9; }
body section, body .popup {
padding: 16px 19px;
background-color: #1f1f1f;
border-radius: 6px;
margin-top: 24px; }
body section h3, body .popup h3 {
margin-top: 0px; }
body .popup {
padding: 8px 12px;
margin-top: 0px;
display: inline-block; }
body .popup label, body .popup input, body .popup button {
margin-right: 12px;
padding: 0.2em 0.6em !important; }
body .popup input {
width: 400px; }
body .pure-button {
background-color: #000000;
border: 1px solid #0B7474;
color: white; }
body .pure-button.inline {
padding: 3px 9px;
margin: 0px 4px; }
body .wrapper {
max-width: 960px;
margin: 0px auto; }
body .header, body .contents, body .subtext {
padding: 18px; }
body .subtext {
background-color: #1f1f1f;
padding: 4px 18px; }
body .progress-bar {
position: relative;
border-radius: 8px;
margin: 16px 0px;
overflow: hidden;
background-color: #06282D;
padding: 4px 3px;
height: 24px; }
body .progress-fill {
border-radius: 8px;
background-color: #0a8071;
height: 24px; }
body .progress-text {
position: absolute;
top: 4px;
bottom: 0px;
left: 0px;
right: 0px;
text-align: center;
font-size: 18px;
text-shadow: 0px 0px 3px #000000;
-webkit-text-shadow: 0px 0px 3px #000000;
-moz-text-shadow: 0px 0px 3px #000000;
-o-text-shadow: 0px 0px 3px #000000;
-ms-text-shadow: 0px 0px 3px #000000; }
body .progress-container {
position: relative;
height: 24px;
margin: 28px 0px; }
body .progress-container label {
position: absolute;
top: 0px;
left: 0px;
font-size: 19px;
font-weight: bold;
margin-top: 4px; }
body .progress-container .progress-bar {
position: absolute;
left: 250px;
right: 0px;
margin: 0px; }
body .progress-container .progress-text {
text-align: left;
margin-left: 16px; }
body .header {
background-color: black;
overflow: hidden;
height: 42px; }
body .header h1 {
margin: 0px;
display: inline-block;
font-size: 32px; }
body .header h1 a {
text-decoration: inherit;
color: inherit; }
body .header .pure-button {
background-color: #3C3C3C; }
body .header .button-upload, body .header .button-lite {
/*display: inline-block;
margin-left: 32px;
vertical-align: 5px;*/
float: right; }
body .header .button-lite {
border: 1px solid transparent;
background: none;
margin-right: 4px; }
body .header .button-lite:hover {
background-color: #3C3C3C;
border: 1px solid #0B7474; }
body .header .abuse {
float: right;
margin-right: 64px;
margin-top: 10px; }
body .contents h2 {
margin-top: 0px; }
body .contents #upload_activator {
margin-bottom: 18px; }
body .contents .latest {
margin-top: 64px; }
body .contents .latest a.document {
display: inline-block;
width: 150px;
height: 218px;
background-color: white;
text-decoration: none;
border: 0px;
margin-right: 6px; }
body .contents .latest a.document img {
width: 150px; }
body .contents .latest a.gallery-link {
display: block;
float: right;
padding: 12px 64px; }
body .contents .upload-form {
font-size: 18px;
text-align: center;
margin-top: 38px; }
body .contents .upload-form .button-browse {
font-size: 24px;
vertical-align: -1px;
margin-right: 24px; }
body .contents .upload-form .faded {
opacity: 0.4; }
body .contents .upload-form .info, body .contents .upload-form .fileinfo, body .contents .upload-form .progress {
text-align: left;
max-width: 700px;
margin: 64px auto 0px auto; }
body .contents .upload-form .fileinfo {
display: none; }
body .contents .upload-form .fileinfo h2 {
margin: 0px 0px 16px 0px; }
body .contents .upload-form .fileinfo label {
margin-left: 7px; }
body .contents .upload-form .fileinfo .button-submit {
float: right;
margin-top: 16px; }
body .contents .upload-form .progress {
display: none;
margin-top: 32px; }
body .contents .upload-form .progress .wait {
display: none; }
body .contents .upload-form #uploadError {
background-color: #CD1E32;
padding: 20px 28px;
text-align: left;
width: 700px;
margin: 0px auto;
display: none;
margin-top: 16px; }
body .contents .upload-form #uploadError h3 {
margin-top: 0px;
margin-bottom: 7px; }
body .contents .upload-form #uploadError p {
margin: 0px; }
body .contents .upload-form #upload_element {
display: none; }
body .contents .upload-form .bar, body .contents .upload-form .bar-inner {
height: 12px; }
body .contents .upload-form .bar {
border: 1px solid #0B7474;
border-radius: 4px;
overflow: hidden;
margin-top: 8px; }
body .contents .upload-form .bar-inner {
background-color: #014949;
width: 0%;
border-radius: 3px; }
body .viewer-contents .embed_code, body .viewer-contents .link_code {
background-color: #1C1C1C;
border: 1px solid black;
border-radius: 4px;
padding: 4px;
color: #E1E1E1; }
body .viewer-contents textarea.embed_code, body .viewer-contents .link_code {
width: 247px;
margin-top: 7px; }
body .viewer-contents input.embed_code {
width: 275px; }
body .viewer-contents .viewer-wrapper {
position: absolute;
top: 78px;
/*top: 117px;*/
bottom: 0px;
left: 0px;
right: 320px;
overflow: hidden; }
body .viewer-contents .viewer-wrapper .viewer {
width: 100%;
height: 100%;
border: 0px; }
body .viewer-contents .bottombar {
display: none;
position: absolute;
bottom: 0px;
left: 0px;
right: 0px;
height: 48px;
padding: 8px; }
body .viewer-contents .bottombar .header-wrapper {
width: 99%; }
body .viewer-contents .bottombar .header-wrapper h2 {
display: block;
margin: 0px 0px 3px 0px;
font-size: 17px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis; }
body .viewer-contents .bottombar .tools {
font-size: 14px; }
body .viewer-contents .bottombar .tools .views {
float: left;
margin-top: 4px;
font-weight: bold; }
body .viewer-contents .bottombar .tools .embed {
float: right;
margin-right: 48px; }
body .viewer-contents .bottombar .tools .download {
float: right; }
body .viewer-contents .sidebar {
position: absolute;
top: 78px;
/*top: 117px;*/
bottom: 0px;
right: 0px;
width: 320px;
box-sizing: border-box;
-moz-box-sizing: border-box;
overflow-y: auto; }
body .viewer-contents .sidebar .actual-contents {
padding: 16px; }
body .viewer-contents .sidebar h2 {
margin: 6px 0px 0px 0px;
overflow: hidden;
text-overflow: ellipsis; }
body .viewer-contents .sidebar .embed_code {
height: 100px; }
body .viewer-contents .sidebar .download-box, body .viewer-contents .sidebar .embed-box, body .viewer-contents .sidebar .link-box {
padding: 13px 16px;
margin-top: 24px;
border-radius: 4px; }
body .viewer-contents .sidebar .download-box {
background-color: #0b4d56; }
body .viewer-contents .sidebar .download-box .formats {
margin-top: 12px; }
body .viewer-contents .sidebar .embed-box, body .viewer-contents .sidebar .link-box {
background-color: #3a3e45; }
body .viewer-contents .sidebar h3 {
margin: 0px 0px 4px 0px; }
body .viewer-contents .sidebar p {
margin-top: 4px;
margin-bottom: 0px; }
body .viewer-contents .sidebar .donation-box {
padding: 0px 16px 16px 16px;
background-color: black;
text-align: center; }
body .viewer-contents .sidebar .donation-box h3 {
font-size: 18px;
margin-bottom: 8px; }
body .viewer-contents .sidebar .donation-box p.amounts {
font-size: 21px;
margin-bottom: 12px; }
body .viewer-contents .sidebar .donation-box .donation-buttons {
margin-top: 16px;
font-size: 15px; }
body .viewer-contents .sidebar .donation-box .donation-buttons a.pure-button {
margin-right: 16px;
padding: .4em .9em; }
body .viewer-contents .sidebar .toolbar-settings {
margin: 8px 3px; }
body .viewer-contents .sidebar .toolbar-settings input {
position: relative;
top: 2px;
margin-right: 6px; }
body .viewer-contents .sidebar .toolbar-settings label {
margin-right: 14px;
margin-top: -2px;
font-size: 14px; }
body .viewer-contents .sidebar .stats {
margin-top: 8px;
font-size: 13px;
padding: 5px 8px;
text-align: center; }
body.announcement-visible .viewer-contents .viewer-wrapper, body.announcement-visible .viewer-contents .sidebar {
top: 117px; }
body .gallery {
background-color: #242427;
border: 1px solid black; }
body .gallery .next, body .gallery .previous {
display: block;
padding: 16px;
font-size: 18px; }
body .gallery .next {
float: right; }
body .gallery .previous {
float: left; }
body .gallery .document {
padding: 24px;
border-bottom: 1px solid black;
display: block;
color: white; }
body .gallery .document:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden; }
body .gallery .document:hover {
background-color: #2a2a2e; }
body .gallery .document span {
display: block; }
body .gallery .document span.thumb {
width: 83px;
height: 120px;
float: left;
background-color: white;
margin-right: 16px; }
body .gallery .document span.thumb img {
height: 120px; }
body .gallery .document .name {
font-weight: bold;
font-size: 24px; }
body .gallery .document .date {
font-size: 20px; }
body .gallery .document .views {
font-size: 18px; }
body .donation-page .js-available {
display: none; }
body .donation-page .bip21-qr {
float: right;
margin: -25px 12px 12px 12px; }
body .donation-page section:after {
content: "";
display: table;
clear: both; }
body .donation-page section.instructions {
min-height: 200px; }
body .donation-page .option {
float: left;
padding: 14px;
border: 1px solid gray;
margin: 5px;
width: 190px;
height: 60px;
border-radius: 6px;
text-align: center;
font-size: 20px;
position: relative;
background-color: #0D0D0D; }
body .donation-page .option.payment-method {
height: 75px; }
body .donation-page .option.payment-method label {
font-size: 14px; }
body .donation-page .option.payment-method label.fixed {
position: absolute;
bottom: 21px;
left: 0px;
right: 0px;
text-align: center; }
body .donation-page .option.payment-method label.logo {
font-size: 33px;
font-weight: bold;
color: #efefef; }
body .donation-page .option.selected {
background-color: #003D35; }
body .donation-page .option label, body .donation-page .option input {
display: block;
margin: 0px auto; }
body .donation-page .option label {
margin-bottom: 9px; }
body .donation-page .option input[type="radio"] {
position: absolute;
bottom: 12px;
left: 50%;
margin-left: -6px; }
body .donation-page .option .exchange-rate {
font-size: 15px; }
body .donation-page .option #custom_amount_input {
width: 72px;
display: inline;
margin-left: 5px;
background-color: #202020;
border: 1px solid #474747;
color: white;
padding: 4px 4px 3px 4px;
font-size: 19px; }
body .donation-page .paypal-button {
text-align: center;
padding: 24px;
background-color: #0D0D0D; }
body .donation-page .paypal-button:hover {
background-color: black; }
body .blog-post h2, body .blog-index h2 {
font-size: 29px; }
body .blog-index .post {
margin-bottom: 9px; }
body .blog-index .date {
font-family: monospace;
margin-right: 16px;
color: #dbdbdb;
font-size: 15px; }
body .blog-index .title {
margin-left: 8px; }
body .blog-post section {
padding: 1px 24px; }
body .blog-post a.anchor {
float: left;
margin-left: -16px;
margin-top: 4px;
font-weight: normal;
font-size: 80%; }
body .blog-post h3 {
font-size: 24px; }
body .blog-post h4 {
font-size: 19px; }
body .admin {
position: relative; }
body .admin th, body .admin td {
padding: 4px 7px; }
body .admin th {
text-align: left; }
body .admin td {
border-top: 1px solid gray; }
body .admin .save-button {
margin-right: 8px; }
body .admin form.pure-g [class*="pure-u"] {
box-sizing: border-box;
padding: 5px; }
body .admin form.pure-g label {
font-weight: bold;
font-size: 18px;
margin-bottom: 8px; }
body .admin form.pure-g .md-editor {
font-family: monospace; }
body .admin form.pure-g .md-preview {
border: 1px solid #575757;
padding: 8px 16px;
border-radius: 5px; }
body .admin form.pure-g .submit {
position: absolute;
right: 0px;
margin-top: -6px; }
body .error h2 {
font-size: 22px; }
body .pure-button img.icon {
position: relative;
top: 1px;
margin-right: 6px; }
.footer {
margin-top: 32px;
padding-top: 12px;
border-top: 1px solid #DEDEDE;
font-size: 14px;
color: #DEDEDE; }
#drag_ghost {
display: none;
position: absolute;
z-index: 999;
padding: 8px;
background-color: #1D3030;
border: 1px solid #0B7474;
box-shadow: 3px 3px 8px 0px #131314;
color: white;
font-size: 19px;
font-weight: bold;
border-radius: 5px; }
@media (max-width: 570px) {
.button-lite.hide-x-small {
display: none; } }
@media (max-width: 640px) {
.bottombar .embed {
display: none; } }
@media (max-width: 660px) {
.alt-small {
display: block; }
.alt-large {
display: none; } }
@media (min-width: 650px) {
.alt-small {
display: none; }
.alt-large {
display: block; } }
@media (max-width: 800px) {
.dragdrop-instructions {
display: block; } }
@media (max-width: 1000px) {
.hide-small {
display: none; } }
@media (max-width: 1200px) {
body .viewer-contents .sidebar {
display: none; }
body .viewer-contents .viewer-wrapper {
right: 0px;
bottom: 64px; }
body .viewer-contents .bottombar {
display: block; } }
@media (max-height: 940px) {
body .header {
padding: 10px 18px; }
body .viewer-contents .viewer-wrapper, body .viewer-contents .sidebar {
top: 62px; }
body .viewer-contents .sidebar h2 {
margin: 0px;
font-size: 20px; }
body .viewer-contents .sidebar h3 {
font-size: 18px;
margin: 0px; }
body .viewer-contents .sidebar p {
margin: 1px 0px; }
body .viewer-contents .sidebar .embed-box, body .viewer-contents .sidebar .link-box, body .viewer-contents .sidebar .download-box {
margin: 16px 0px;
padding: 8px 16px 10px 16px; }
body .viewer-contents .sidebar .donation-box {
padding-top: 8px; }
body .viewer-contents .sidebar textarea.embed_code {
height: 32px; }
body .viewer-contents .sidebar .stats {
margin-top: 4px;
padding: 0px 8px; }
body.announcement-visible .viewer-contents .viewer-wrapper, body.announcement-visible .viewer-contents .sidebar {
top: 101px; } }
.clear {
clear: both; }
.notice {
background-color: #086458;
color: white;
padding: 8px 14px;
margin-bottom: 32px;
border-radius: 4px; }
.notice a {
color: white; }
.notice p {
margin: 8px 0px; }
.announce {
background-color: #0A8071;
color: white;
text-align: center;
padding: 12px 16px;
font-size: 18px; }
.announce a {
color: white; }
.full-screen .announce {
height: 21px;
overflow: hidden;
font-size: 17px;
padding: 9px 16px; }
::-webkit-scrollbar {
background-color: white; }
::-webkit-scrollbar-track {
background-color: #5A5A61; }
::-webkit-scrollbar-thumb {
background-color: #171717; }
::-webkit-scrollbar-track:vertical {
border-left: 1px solid #323235; }
::-webkit-scrollbar-thumb-vertical {
border-left: 1px solid #323235; }
::-webkit-scrollbar-track:horizontal {
border-top: 1px solid #323235; }
::-webkit-scrollbar-thumb-horizontal {
border-top: 1px solid #323235; }
::-webkit-scrollbar-button {
background-color: #1C1C1C;
color: white;
background-position: 0px -1px; }
::-webkit-scrollbar-button:vertical:increment {
background-image: url(/static/pdfjs/images/arrow-down.png);
border-top: 1px solid black; }
::-webkit-scrollbar-button:vertical:decrement {
background-image: url(/static/pdfjs/images/arrow-up.png);
border-bottom: 1px solid black; }
::-webkit-scrollbar-button:horizontal:increment {
background-image: url(/static/pdfjs/images/arrow-right.png);
border-left: 1px solid black; }
::-webkit-scrollbar-button:horizontal:decrement {
background-image: url(/static/pdfjs/images/arrow-left.png);
border-right: 1px solid black; }

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save