Ejemplo n.º 1
0
    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,
            )
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
    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("/")
Ejemplo n.º 4
0
    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}")
Ejemplo n.º 5
0
    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}"),
                }
            )
Ejemplo n.º 6
0
    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,
            )
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
    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("/")
Ejemplo n.º 9
0
    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())
Ejemplo n.º 10
0
    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,
                }
            )
Ejemplo n.º 11
0
    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,
            )
Ejemplo n.º 12
0
    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}")
Ejemplo n.º 13
0
    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)
Ejemplo n.º 14
0
    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"
                }]))
Ejemplo n.º 15
0
    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}"),
            })
Ejemplo n.º 16
0
    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")))
Ejemplo n.º 17
0
    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)
Ejemplo n.º 18
0
    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}")
Ejemplo n.º 19
0
    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})
Ejemplo n.º 20
0
    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}")