async def get(self, slug: str) -> None: # type: ignore with database.session() as session: paste = (session.query( database.Paste).filter(database.Paste.slug == slug).first()) if not paste: raise tornado.web.HTTPError(404) can_delete = self.get_cookie("removal") == str(paste.removal) self.render( "show.html", paste=paste, pagetitle=f"View paste {paste.slug}", can_delete=can_delete, linenos=False, )
def add(lexer: str) -> None: """Add a paste to pinnwand's database from stdin.""" from pinnwand import database from pinnwand import utility if lexer not in utility.list_languages(): log.error("add: unknown lexer") return paste = database.Paste(expiry=timedelta(days=1)) file = database.File(sys.stdin.read(), lexer=lexer) paste.files.append(file) with database.session() as session: session.add(paste) session.commit() log.info("add: paste created: %s", paste.slug)
async def get(self, removal_id: str) -> None: """Look up if the user visiting this page has the removal id for a certain paste. If they do they're authorized to remove the paste.""" with database.session() as session: paste = (session.query(database.Paste).filter( database.Paste.removal_id == removal_id).first()) if not paste: log.info("RemovePaste.get: someone visited with invalid id") self.set_status(404) self.render("404.html", pagetitle="404") return session.delete(paste) session.commit() self.redirect("/")
async def post(self) -> None: """This is a historical endpoint to create pastes, pastes are marked as old-web and will get a warning on top of them to remove any access to this route. pinnwand has since evolved with an API which should be used and a multi-file paste. See the 'CreateAction' for the new-style creation of pastes.""" lexer = self.get_body_argument("lexer") raw = self.get_body_argument("code", strip=False) expiry = self.get_body_argument("expiry") if lexer not in utility.list_languages(): log.info("Paste.post: a paste was submitted with an invalid lexer") raise tornado.web.HTTPError(400) # Guard against empty strings if not raw.strip(): return self.redirect(f"/+{lexer}") if expiry not in utility.expiries: log.info( "Paste.post: a paste was submitted with an invalid expiry") raise tornado.web.HTTPError(400) paste = database.Paste(utility.expiries[expiry], "deprecated-web") file = database.File(raw, lexer) file.slug = paste.slug paste.files.append(file) with database.session() as session: session.add(paste) session.commit() # The removal cookie is set for the specific path of the paste it is # related to self.set_cookie("removal", str(paste.removal), path=f"/{paste.slug}") # Send the client to the paste self.redirect(f"/{paste.slug}")
async def post(self) -> None: lexer = self.get_body_argument("lexer") raw = self.get_body_argument("code", strip=False) expiry = self.get_body_argument("expiry") filename = self.get_body_argument("filename", None) if not raw or not raw.strip(): log.info("APINew.post: a paste was submitted without content") raise tornado.web.HTTPError(400) if lexer not in utility.list_languages(): log.info("APINew.post: a paste was submitted with an invalid lexer") raise tornado.web.HTTPError(400) if expiry not in configuration.expiries: log.info( "APINew.post: a paste was submitted with an invalid expiry" ) raise tornado.web.HTTPError(400) paste = database.Paste( utility.slug_create(), configuration.expiries[expiry], "deprecated-api", ) paste.files.append(database.File(paste.slug, raw, lexer, filename)) with database.session() as session: session.add(paste) session.commit() req_url = self.request.full_url() location = paste.slug if filename: location += "#" + url_escape(filename) self.write( { "paste_id": paste.slug, "removal_id": paste.removal, "paste_url": urljoin(req_url, f"/{location}"), "raw_url": urljoin(req_url, f"/raw/{paste.files[0].slug}"), } )
async def get(self, paste_id: str) -> None: with database.session() as session: paste = (session.query(database.Paste).filter( database.Paste.paste_id == paste_id).first()) if not paste: self.set_status(404) self.render("404.html", pagetitle="404") return can_delete = self.get_cookie("removal") == str(paste.removal_id) self.render( "show.html", paste=paste, pagetitle="show", can_delete=can_delete, linenos=False, )
def slug_create(auto_scale: bool = True) -> str: """Creates a new slug, a slug has to be unique within both the Paste and File namespace. These slugs auto-lengthen unless they are specified not to.""" with database.session() as session: if auto_scale: # We count our new paste as well count = (session.query(database.Paste).count() + session.query(database.File).count() + 1) # The amount of bits necessary to store that count times two, then # converted to bytes with a minimum of 1. # We double the count so that we always keep half of the space # available (e.g we increase the number of bytes at 127 instead of # 255). This ensures that the probing below can find an empty space # fast in case of collision. necessary = math.ceil(math.log2(count * 2)) // 8 + 1 else: necessary = 16 # 16 bytes should do, right? # Now generate random ids in the range with a maximum amount of # retries, continuing until an empty slot is found tries = 0 slug = hash_create(necessary) # If a slug exists in either the Paste or File namespace create a new # one. while any(( session.query( database.Paste).filter_by(slug=slug).one_or_none(), session.query( database.File).filter_by(slug=slug).one_or_none(), )): log.debug("slug_create: triggered a collision") if tries > 10: raise RuntimeError( "We exceeded our retry quota on a collision.") tries += 1 slug = hash_create(necessary) return slug
async def get(self, removal: str) -> None: # type: ignore """Look up if the user visiting this page has the removal id for a certain paste. If they do they're authorized to remove the paste.""" with database.session() as session: paste = ( session.query(database.Paste) .filter(database.Paste.removal == removal) .first() ) if not paste: log.info("RemovePaste.get: someone visited with invalid id") raise tornado.web.HTTPError(404) session.delete(paste) session.commit() self.redirect("/")
async def get(self, paste_id: str) -> None: # type: ignore """Get all files from the database and download them as a zipfile.""" with database.session() as session: paste = ( session.query(database.Paste) .filter(database.Paste.slug == paste_id) .first() ) if not paste: raise tornado.web.HTTPError(404) if paste.exp_date < datetime.utcnow(): session.delete(paste) session.commit() log.warn( "FileRaw.get: paste was expired, is your cronjob running?" ) raise tornado.web.HTTPError(404) data = io.BytesIO() with zipfile.ZipFile(data, "x") as zf: for file in paste.files: if file.filename: filename = f"{utility.filename_clean(file.filename)}-{file.slug}.txt" else: filename = f"{file.slug}.txt" zf.writestr(filename, file.raw) data.seek(0) self.set_header("Content-Type", "application/zip") self.set_header( "Content-Disposition", f"attachment; filename={paste.slug}.zip" ) self.write(data.read())
async def get(self, slug: str) -> None: # type: ignore with database.session() as session: paste = ( session.query(database.Paste) .filter(database.Paste.slug == slug) .first() ) if not paste: raise tornado.web.HTTPError(404) self.write( { "paste_id": paste.slug, "raw": paste.files[0].raw, "fmt": paste.files[0].fmt, "lexer": paste.files[0].lexer, "expiry": paste.exp_date.isoformat(), "filename": paste.files[0].filename, } )
async def get(self, slug: str) -> None: # type: ignore """Render the new paste form, optionally have a lexer preselected from the URL.""" with database.session() as session: paste = (session.query( database.Paste).filter(database.Paste.slug == slug).first()) if not paste: raise tornado.web.HTTPError(404) lexers_available = utility.list_languages() await self.render( "create.html", lexers=["text"], # XXX make this majority of file lexers? lexers_available=lexers_available, pagetitle="repaste", message=None, paste=paste, )
async def get(self, slug: str) -> None: # type: ignore """Fetch paste from database and redirect to /slug if the paste exists.""" with database.session() as session: paste = (session.query( database.Paste).filter(database.Paste.slug == slug).first()) if not paste: raise tornado.web.HTTPError(404) if paste.exp_date < datetime.now(): session.delete(paste) session.commit() log.warn( "RedirectShow.get: paste was expired, is your cronjob running?" ) raise tornado.web.HTTPError(404) self.redirect(f"/{paste.slug}")
async def get(self, file_id: str) -> None: # type: ignore """Get a file from the database and show it in the plain.""" with database.session() as session: file = (session.query( database.File).filter(database.File.slug == file_id).first()) if not file: raise tornado.web.HTTPError(404) if file.paste.exp_date < datetime.now(): session.delete(file.paste) session.commit() log.warn( "FileRaw.get: paste was expired, is your cronjob running?") raise tornado.web.HTTPError(404) self.set_header("Content-Type", "text/plain; charset=utf-8") self.write(file.raw)
async def post(self) -> None: with database.session() as session: paste = (session.query(database.Paste).filter( database.Paste.removal == self.get_body_argument( "removal_id")).first()) if not paste: self.set_status(400) return session.delete(paste) session.commit() # this is set this way because tornado tries to protect us # by not allowing lists to be returned, looking at this code # it really shouldn't be a list but we have to keep it for # backwards compatibility self.set_header("Content-Type", "application/json") self.write( json.dumps([{ "paste_id": paste.slug, "status": "removed" }]))
async def post(self) -> None: lexer = self.get_body_argument("lexer") raw = self.get_body_argument("code") expiry = self.get_body_argument("expiry") filename = self.get_body_argument("filename", None) if not raw: log.info("APINew.post: a paste was submitted without content") raise tornado.web.HTTPError(400) if lexer not in utility.list_languages(): log.info( "APINew.post: a paste was submitted with an invalid lexer") raise tornado.web.HTTPError(400) if expiry not in utility.expiries: log.info( "APINew.post: a paste was submitted with an invalid expiry") raise tornado.web.HTTPError(400) paste = database.Paste(raw, lexer, utility.expiries[expiry], "api", filename) with database.session() as session: session.add(paste) session.commit() req_url = self.request.full_url() location = paste.paste_id if filename: location += "#" + url_escape(filename) self.write({ "paste_id": paste.paste_id, "removal_id": paste.removal_id, "paste_url": urljoin(req_url, f"/show/{location}"), "raw_url": urljoin(req_url, f"/raw/{location}"), })
async def get(self, file_id: str) -> None: # type: ignore """Get a file from the database and show it in hex.""" if defensive.ratelimit(self.request, area="read"): raise error.RatelimitError() with database.session() as session: file = (session.query( database.File).filter(database.File.slug == file_id).first()) if not file: raise tornado.web.HTTPError(404) if file.paste.exp_date < datetime.utcnow(): session.delete(file.paste) session.commit() log.warn( "FileRaw.get: paste was expired, is your cronjob running?") raise tornado.web.HTTPError(404) self.set_header("Content-Type", "text/plain; charset=utf-8") self.write(binascii.hexlify(file.raw.encode("utf8")))
async def get(self, file_id: str) -> None: # type: ignore """Get a file from the database and download it in the plain.""" with database.session() as session: file = ( session.query(database.File) .filter(database.File.slug == file_id) .first() ) if not file: raise tornado.web.HTTPError(404) if file.paste.exp_date < datetime.now(): session.delete(file.paste) session.commit() log.warn( "FileDownload.get: paste was expired, is your cronjob running?" ) raise tornado.web.HTTPError(404) self.set_header("Content-Type", "text/plain; charset=utf-8") if file.filename: filename = ( f"{utility.filename_clean(file.filename)}-{file.slug}.txt" ) else: filename = f"{file.slug}.txt" self.set_header( "Content-Disposition", f"attachment; filename={filename}" ) self.write(file.raw)
def post(self) -> None: # type: ignore """POST handler for the 'web' side of things.""" expiry = self.get_body_argument("expiry") if expiry not in configuration.expiries: log.info( "CreateAction.post: a paste was submitted with an invalid expiry" ) raise error.ValidationError() auto_scale = self.get_body_argument("long", None) is None lexers = self.get_body_arguments("lexer") raws = self.get_body_arguments("raw", strip=False) filenames = self.get_body_arguments("filename") if not all([lexers, raws, filenames]): # Prevent empty argument lists from making it through raise error.ValidationError() if not all(raw.strip() for raw in raws): # Prevent empty raws from making it through raise error.ValidationError() if any(len(L) != len(lexers) for L in [lexers, raws, filenames]): log.info("CreateAction.post: mismatching argument lists") raise error.ValidationError() if any(lexer not in utility.list_languages() for lexer in lexers): log.info("CreateAction.post: a file had an invalid lexer") raise error.ValidationError() with database.session() as session, utility.SlugContext( auto_scale ) as slug_context: paste = database.Paste( next(slug_context), configuration.expiries[expiry], "web" ) for (lexer, raw, filename) in zip(lexers, raws, filenames): paste.files.append( database.File( next(slug_context), raw, lexer, filename if filename else None, ) ) if sum(len(f.fmt) for f in paste.files) > configuration.paste_size: log.info("CreateAction.post: sum of files was too large") raise error.ValidationError() # For the first file we will always use the same slug as the paste, # since slugs are generated to be unique over both pastes and files # this can be done safely. paste.files[0].slug = paste.slug session.add(paste) session.commit() # The removal cookie is set for the specific path of the paste it is # related to self.set_cookie( "removal", str(paste.removal), path=f"/{paste.slug}" ) # Send the client to the paste self.redirect(f"/{paste.slug}")
async def post(self) -> None: if defensive.ratelimit(self.request, area="create"): raise error.RatelimitError() try: data = tornado.escape.json_decode(self.request.body) except json.decoder.JSONDecodeError: raise tornado.web.HTTPError(400, "could not parse json body") expiry = data.get("expiry") if expiry not in configuration.expiries: log.info( "Paste.post: a paste was submitted with an invalid expiry") raise tornado.web.HTTPError(400, "invalid expiry") auto_scale = data.get("long", None) is None files = data.get("files", []) if not files: raise tornado.web.HTTPError(400, "no files provided") with database.session() as session, utility.SlugContext( auto_scale) as slug_context: paste = database.Paste( next(slug_context), configuration.expiries[expiry], "v1-api", ) for file in files: lexer = file.get("lexer", "") content = file.get("content") filename = file.get("name") if lexer not in utility.list_languages(): raise tornado.web.HTTPError(400, "invalid lexer") if not content: raise tornado.web.HTTPError(400, "invalid content (empty)") try: paste.files.append( database.File( next(slug_context), content, lexer, filename, )) except error.ValidationError: raise tornado.web.HTTPError( 400, "invalid content (exceeds size limit)") if sum(len(f.fmt) for f in paste.files) > configuration.paste_size: raise tornado.web.HTTPError( 400, "invalid content (exceeds size limit)") paste.files[0].slug = paste.slug session.add(paste) try: session.commit() except Exception: # XXX be more precise log.warning("%r", slug_context._slugs) raise # Send the client to the paste url_request = self.request.full_url() url_paste = urljoin(url_request, f"/{paste.slug}") url_removal = urljoin(url_request, f"/remove/{paste.removal}") self.write({"link": url_paste, "removal": url_removal})
def post(self) -> None: # type: ignore """POST handler for the 'web' side of things.""" expiry = self.get_body_argument("expiry") if expiry not in utility.expiries: log.info( "CreateAction.post: a paste was submitted with an invalid expiry" ) raise error.ValidationError() auto_scale = self.get_body_argument("long", None) is None lexers = self.get_body_arguments("lexer") raws = self.get_body_arguments("raw", strip=False) filenames = self.get_body_arguments("filename") if not all([lexers, raws, filenames]): # Prevent empty argument lists from making it through raise error.ValidationError() if not all(raw.strip() for raw in raws): # Prevent empty raws from making it through raise error.ValidationError() with database.session() as session: paste = database.Paste(utility.expiries[expiry], "web", auto_scale) if any(len(L) != len(lexers) for L in [lexers, raws, filenames]): log.info("CreateAction.post: mismatching argument lists") raise error.ValidationError() for (lexer, raw, filename) in zip(lexers, raws, filenames): if lexer == 'AUTO': try: lexer = guess_lexer(raw).name except ValueError: # Fall back to plain text lexer = "text" if lexer not in utility.list_languages(): log.info("CreateAction.post: a file had an invalid lexer") raise error.ValidationError() if not raw: log.info("CreateAction.post: a file had an empty raw") raise error.ValidationError() paste.files.append( database.File(raw, lexer, filename if filename else None, auto_scale)) # For the first file we will always use the same slug as the paste, # since slugs are generated to be unique over both pastes and files # this can be done safely. paste.files[0].slug = paste.slug session.add(paste) session.commit() # The removal cookie is set for the specific path of the paste it is # related to self.set_cookie("removal", str(paste.removal), path=f"/{paste.slug}") # Send the client to the paste self.redirect(f"/{paste.slug}")