#!/usr/bin/python3.13
# -*- coding: utf-8 -*-

# Author: Stéphane Graber <stgraber@ubuntu.com>
# Written by Stéphane Graber <stgraber@stgraber.org>
#            Daniel Bartlett <dan@f-box.org>

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

from __future__ import print_function

import argparse
import gettext
import os
import re
import socket
import xml.dom.minidom

import sys
from configparser import ConfigParser, NoOptionError
from urllib.parse import urlencode
from urllib import request

try:
    import json
except ImportError:
    json = None

# Specify program version
version = "1.7.0"

# Set the default pastebin
defaultPB = "bpa.st"

# Now try to override it with a distributor pastebin
try:
    import distro
    release = distro.id()
    if release in ('debian', 'raspbian'):
        defaultPB = "paste.debian.net"
    elif release in ('fedora', 'centos', 'rhel', 'rocky'):
        defaultPB = "paste.centos.org"
except ImportError:
    distro = None
    pass


class PasteRequest(request.Request):
    version = "Pastebinit v%s" % version

    def __init__(self, *args, **opts):
        super(PasteRequest, self).__init__(*args, **opts)
        if 'User-Agent' not in self.headers:
            self.add_header('User-Agent', self.version)


def preloadPastebins():
    # Check several places for config files:
    #  - global config in /etc/pastebin.d
    #  - for source checkout, config in the checkout
    #  - user's overrides in ~/.pastebin.d
    # Files found later override files found earlier.
    pastebind = {}
    confdirs = []
    for confdir in ['/usr/share', '/usr/local/share'] \
            + list(reversed(os.environ.get('XDG_DATA_DIRS', '').split(':'))) \
            + list(reversed(os.environ.get('XDG_CONFIG_DIRS', '').split(':'))) \
            + ['/etc', '/usr/local/etc',
               os.environ.get('XDG_CONFIG_HOME',
                   os.path.expanduser('~/.config')),
               os.path.expanduser('~/.pastebin.d')]:

        confdir = confdir.rstrip('/')
        if not confdir.endswith('pastebin.d'):
            confdir = os.path.join(confdir, 'pastebin.d')
        if confdir not in confdirs:
            confdirs.append(confdir)

    for confdir in confdirs:
        try:
            confdirlist = os.listdir(confdir)
        except OSError:
            continue

        for fileitem in confdirlist:
            if fileitem.startswith('.') or not fileitem.endswith('.conf'):
                continue

            filename = os.path.join(confdir, fileitem)
            instance = ConfigParser()
            try:
                instance.read(filename)
            except UnicodeError:
                continue

            if not instance.has_section('pastebin'):
                print(_('%s: no section [pastebin]') % filename,
                      file=sys.stderr)
                continue

            if not instance.has_option('pastebin', 'basename'):
                print(_("%s: no 'basename' in [pastebin]") % filename,
                      file=sys.stderr)
                continue

            pastebind[instance.get('pastebin', 'basename')] = instance
    return pastebind


# Return the configuration depending of the pastebin used
def getConfig(website):
    for config in list(pastebind.values()):
        basename = config.get('pastebin', 'basename')
        try:
            https = config.get('pastebin', 'https')
        except:
            https = False

        if basename == website:
            if https:
                website = "https://%s/" % basename
            else:
                website = "http://%s/" % basename
            return website, config['pastebin']

    print(_("Unknown website, please post a bugreport to request "
            "this pastebin to be added (%s)") % website,
          file=sys.stderr)
    sys.exit(1)


# Return the parameters depending of the pastebin used
def getParameters(website, content):
    config = pastebind[website]
    params = {}
    for param in config.options('format'):
        paramname = config.get('format', param)
        if param == 'content':
            params[paramname] = content
        else:
            params[paramname] = config.get('defaults', param,
                vars=spec_opts, fallback=inline_opts.get(param))
    return params


# XML Handling methods
def getText(nodelist):
    rc = ""
    for node in nodelist:
        if node.nodeType == node.TEXT_NODE:
            rc = rc + node.data
    return rc


def getNodes(nodes, title):
    return nodes.getElementsByTagName(title)


def getFirstNode(nodes, title):
    return getNodes(nodes, title)[0]


def getFirstNodeText(nodes, title):
    return getText(getFirstNode(nodes, title).childNodes)


def getArgs():
    # Initialize argparser
    parser = argparse.ArgumentParser(
        prog='pastebinit',
        usage=_("%(prog)s [OPTION...] [FILE...]"),
        description=_("Reads on stdin for input or takes a list of files as parameters"),
        add_help=False
    )
    parser.add_argument('files', nargs='*', action='extend', metavar='FILE',
                        type=str, help=argparse.SUPPRESS)

    general = parser.add_argument_group(_("General arguments"))
    general.add_argument('-b', dest='website',
                         type=str, metavar=_("<pastebin>"),
                         help=_("(default is '%s')") % website)
    general.add_argument('-i', nargs='+', action='extend', dest='files',
                         type=str, metavar='FILE',
                         help=_("One or more files to read and paste"))
    general.add_argument('-l', action='store_true', dest='list_pastebins',
                         help=_("List all supported pastebins"))
    general.add_argument('-E', action='store_true', dest='echo',
                         help=_("Print the content to stdout too"))
    general.add_argument('-h', action='help',
                         help=_("Print this help screen"))
    general.add_argument('-v', action='version',
                         help=_("Print the version number"),
                         version="%(prog)s v" + version)
    general.add_argument('-V', action='store_true', dest='verbose',
                         help=_("Print verbose output to stderr"))

    optional = parser.add_argument_group(_("Optional arguments (not supported by all pastebins)"))
    optional.add_argument('-a', dest='user',
                          type=str, metavar=_("<author>"),
                          help=_("(default is '%s')") % spec_opts.get(
                              'user', inline_opts['user']))
    optional.add_argument('-t', dest='title',
                          type=str, metavar=_("<title of paste>"),
                          help=_("(default is '%s')") % inline_opts['title'])
    optional.add_argument('-f', dest='format',
                          type=str, metavar=_("<format of paste>"),
                          help=_("(default is '%s')") % spec_opts.get(
                              'format', inline_opts['format']))
    optional.add_argument('-P', dest='private', default=None,
                          type=str, metavar=_("<private level>"),
                          help=_("(default is '%s')") % spec_opts.get(
                              'private', inline_opts['private']))
    optional.add_argument('-e', dest='expiry', default=None,
                          type=str, metavar=_("<expiry of paste>"),
                          help=_("(default is '%s')") % inline_opts['expiry'])
    optional.add_argument('-u', dest='username',
                          type=str, metavar=_("<username>"),
                          help=_("(default is '%s')") % inline_opts['username'])
    optional.add_argument('-p', dest='password',
                          type=str, metavar=_("<password>"),
                          help=_("(default is '%s')") % inline_opts['password'])

    return parser.parse_args()


if __name__ == "__main__":
    try:
        _ = gettext.gettext
        gettext.textdomain("pastebinit")
    except:
        pass

    try:
        # Timeout after 5s
        socket.setdefaulttimeout(15)

        configfiles = []

        for conffile in list(reversed(os.environ.get(
                'XDG_CONFIG_DIRS', '').split(':'))) + ['/etc', '/usr/local/etc',
                   os.environ.get('XDG_CONFIG_HOME',
                       os.path.expanduser('~/.config')),
                   os.path.expanduser('~/.pastebinit.xml')]:

            conffile = conffile.rstrip('/')
            if not conffile.endswith('pastebinit.xml'):
                conffile = os.path.join(conffile, 'pastebinit.xml')
            if conffile not in configfiles:
                configfiles.append(conffile)

        # Set defaults
        website = defaultPB
        inline_opts = {
            'user'      : os.environ.get('USER', os.environ.get('LOGNAME')),
            'title'     : "",
            'format'    : "text",
            'private'   : "1",
            'expiry'    : "",
            'username'  : "",
            'password'  : ""
        }
        spec_opts = {}
        filenames = []
        verbose = False
        echo = False

        # Example configuration file string
        configexample = """\
    <pastebinit>
        <pastebin>paste.debian.net</pastebin>
        <author>A pastebinit user</author>
        <format>text</format>
        <private>1</private>
        <expiry></expiry>
    </pastebinit>
    """

        for configfile in configfiles:
            # Open configuration file if it exists
            try:
                f = open(configfile)
                configtext = f.read()
                f.close()
                gotconfigxml = True
            except KeyboardInterrupt:
                print(_("KeyboardInterrupt caught."), file=sys.stderr)
                sys.exit(1)
            except:
                gotconfigxml = False

            # Parse configuration file
            if gotconfigxml:
                try:
                    # noinspection PyUnboundLocalVariable
                    configxml = xml.dom.minidom.parseString(configtext)
                    for variable, key in (('pastebin', 'website'),
                                          ('author', 'user'),
                                          ('format', 'format'),
                                          ('private', 'private'),
                                          ('expiry', 'expiry')):
                        try:
                            value = getFirstNodeText(configxml, variable)
                            if variable == 'pastebin':
                                website = value
                            else:
                                spec_opts[key] = value
                        except:
                            pass
                except KeyboardInterrupt:
                    print(_("KeyboardInterrupt caught."), file=sys.stderr)
                    sys.exit(1)
                except:
                    print(_("Error parsing configuration file!"), file=sys.stderr)
                    print(_("Please ensure that your configuration file looks "
                            "similar to the following:"), file=sys.stderr)
                    print(configexample, file=sys.stderr)
                    sys.exit(1)

        # Get options
        try:
            args = getArgs()
        except KeyboardInterrupt:
            print(_("KeyboardInterrupt caught."), file=sys.stderr)
            sys.exit(1)

        # Get the configuration of all pastebins
        pastebind = preloadPastebins()

        # Handle options properly with argparse.
        # list_pastebins triggers an exit, no processing of paste data.
        if args.list_pastebins:
            print(_("Supported pastebins:"))
            for pastebin in sorted(list(pastebind.keys())):
                print("- %s" % pastebin)
            sys.exit(0)

        # These two are bools, with new defaults.
        echo = args.echo
        verbose = args.verbose

        # We do have some optional args to check against though.
        if args.files:
            filenames = args.files
        if args.website:
            website = args.website
        if args.user:
            spec_opts['user'] = args.user
        if args.title:
            spec_opts['title'] = args.title
        if args.format:
            spec_opts['format'] = args.format
        if args.private is not None:
            spec_opts['private'] = args.private
        if args.expiry is not None:
            spec_opts['expiry'] = args.expiry
        if args.username:
            spec_opts['username'] = args.username
        if args.password:
            spec_opts['password'] = args.password

        # Get the configuration
        if '://' in website:
            website = website.split('://')[1]
        website = website.strip('/')
        website, config = getConfig(website)

        if not filenames:
            filenames = ["-"]

        contents = []
        for filename in filenames:
            # If - is specified as a filename read from stdin
            # otherwise load the specified files.
            content = ""
            try:
                if filename == "-":
                    filename = "STDIN"
                    content = sys.stdin.read().rstrip()
                else:
                    with open(filename, "r") as fd:
                        content = fd.read().rstrip()
            except KeyboardInterrupt:
                print(_("KeyboardInterrupt caught."), file=sys.stderr)
                sys.exit(1)
            except:
                print(_("Error reading from: '%s'") % filename, file=sys.stderr)
                sys.exit(1)

            if not content:
                print(_("You are trying to send an empty document, exiting."),
                      file=sys.stderr)
                sys.exit(1)

            contents.append(content)
            if echo:
                print(content)

        if "post_page" in config:
            # Use post page, without leading slash, website has a trailing one.
            post_url = website + config['post_page'].lstrip('/')
        else:
            post_url = website

        if "paste_regexp" in config:
            reLink = config['paste_regexp']
        else:
            reLink = False

        # Get target_page for replacement, only used with reLink.
        if "target_page" in config:
            relink_target_url = website + config['target_page'].lstrip('/')
            if not reLink:
                print("Warning: using target_page without paste_regexp.",
                      file=sys.stderr)
        elif reLink:
            relink_target_url = website

        if "post_format" in config:
            post_format = config['post_format']
        else:
            post_format = 'standard'

        if "user_length" in config:
            user_length = int(config['user_length'])
            user = pastebind[config['basename']].get('defaults', 'user',
                    vars=spec_opts, fallback=inline_opts.get('user'))
            if len(user) > user_length:
                spec_opts['user'] = "%s~" % user[:user_length-1]

        for content in contents:
            # Get the parameters
            params = getParameters(config['basename'], content)

            if "sizelimit" in config:
                if len(content) > int(config['sizelimit']):
                    print(_("The content you are trying to send exceeds "
                            "the pastebin's size limit."), file=sys.stderr)
                    sys.exit(1)

            req = PasteRequest(post_url)

            if post_format == 'json':
                if json:
                    params = bytes(json.dumps(params), encoding='US-ASCII')
                    req.add_header('Content-Type', 'text/json')
                else:
                    print(_("Could not find any JSON library."), file=sys.stderr)
                    sys.exit(1)
            else:
                # Convert to a format usable with the HTML POST
                params = bytes(urlencode(params), encoding='US-ASCII')

            # Send the informations and be redirected to the final page
            if verbose:
                print("POSTing to: %s\nParams: %s" % (
                      post_url, str(params)), file=sys.stderr)
            try:
                page = request.urlopen(req, params)
            except Exception as e:
                print(_("Failed to contact the server: %s") % e, file=sys.stderr)
                sys.exit(1)

            try:
                # Check if we have to apply a regexp
                paste_url = ""
                if reLink:
                    result = page.read().decode('utf-8').strip()
                    if reLink == '(.*)':
                        paste_url = result
                    else:
                        # Print the result of the regexp
                        result = re.split(reLink, result)[1]
                        if '%s' not in relink_target_url:
                            paste_url = relink_target_url + result
                        else:
                            paste_url = relink_target_url % result
                else:
                    # Get the final page
                    paste_url = page.url

                # Print the paste URL
                if echo:
                    print("-" * len(paste_url))
                print(paste_url)
            except KeyboardInterrupt:
                print(_("KeyboardInterrupt caught."), file=sys.stderr)
                sys.exit(1)
            except:
                print(_("Unable to read or parse the result page, it could be a "
                        "server timeout or a change server side, "
                        "try with another pastebin."), file=sys.stderr)
                sys.exit(1)

    except KeyboardInterrupt:
        print(_("KeyboardInterrupt caught."), file=sys.stderr)
        sys.exit(1)
