#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mechanize import Browser
from argparse import ArgumentParser, Action, ArgumentTypeError
from re import compile, IGNORECASE, MULTILINE
from datetime import datetime
from getpass import getpass
from os import access, W_OK
from os.path import isdir
from sys import exit, stderr, stdout, version_info

if version_info[2] > 8:
    from ssl import CertificateError
else:
    class CertificateError(Exception):
        pass

class check_dir(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        directory = values
        if not isdir(directory):
            raise ArgumentTypeError("{0} is not a valid path".format(directory))
        if access(directory, W_OK):
            setattr(namespace, self.dest, directory)
        else:
            raise ArgumentTypeError("{0} is not a writable directory".format(directory))

def check_address(address):
    if not address or address.strip() == "":
        raise ValueError('address required')

    # These regex come from the Django project
    # (See https://github.com/django/django/blob/master/django/core/validators.py).
    ul = '\u00a1-\uffff'
    ipv4_re = r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
    hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]*[a-z' + ul + r'0-9])?'
    domain_re = r'(?:\.[a-z' + ul + r'0-9]+(?:[a-z' + ul + r'0-9-]*[a-z' + ul + r'0-9]+)*)*'
    tld_re = r'\.[a-z' + ul + r']{2,}\.?'
    host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'
    regex = compile(
        r'(?:' + ipv4_re + '|' + host_re + ')'
        r'$', IGNORECASE
    )

    if not regex.match(address):
        raise ValueError('not a valid address')

def check_username(username):
    if not username or username.strip() == "":
        raise ValueError('username required')

def check_password(password):
    if not password or password.strip() == "":
        raise ValueError('password required')

def check_otp(otp):
    if not otp or otp.strip() == "":
        raise ValueError('OTP required')

def check_field(checker, data):
    try:
        checker(data)
    except ValueError, e:
        stderr.write("[x] " + str(e) + "\n")
        exit(1)
    return data

def check_prompt_field(checker, field = "", password = False):
    while True:
        try:
            if password:
                data = getpass()
            else:
                stdout.write("[*] " + field)
                data = raw_input()
            checker(data)
            break
        except ValueError, e:
            stdout.write("[x] " + str(e) + "\n")
    return data

if __name__ == "__main__":
    ap = ArgumentParser(
            description="Do a backup of your WordPress data",
            epilog="Example: ./wp-backup-data -a blog.example.net -u user -P")
    ap.add_argument("-u", "--user", help="username to use")
    ap.add_argument("-p", "--password", help="password to use")
    ap.add_argument("-P", "--prompt-for-password", dest="prompt_pwd", action="store_true", help="prompt for password to use")
    ap.add_argument("-O", "--prompt-for-otp", dest="prompt_otp", action="store_true", help="prompt for Yubikey OTP to use")
    ap.add_argument("-a", "--address", help="root address of the WordPress blog (examples: 'blog.example.net' or '192.168.20.53')")
    ap.add_argument("-d", "--directory", action=check_dir, default=".", help="directory where the backup file will be stored")
    ap.add_argument("--http", dest="https", action="store_false", help="use HTTP as protocol")
    ap.add_argument("--https", dest="https", action="store_true", help="use HTTPS as protocol (default)")
    ap.add_argument("--ignore-certificate", dest="ignore-cert", action="store_true", help="ignore invalid certificates")
    ap.add_argument("-v", "--version", action="version", version="%(prog)s 1.3.0")
    ap.set_defaults(https=True)
    ap.set_defaults(ssl=False)
    ap.set_defaults(prompt_pwd=False)

    try:
        args = vars(ap.parse_args())

        if not args["address"]:
            address = check_prompt_field(check_address, 'Address: ')
        else:
            address = check_field(check_address, args["address"])

        if not args["user"]:
            user = check_prompt_field(check_username, 'Username: ')
        else:
            user = check_field(check_username, args["user"])

        if args["prompt_pwd"] or not args["password"]:
            password = check_prompt_field(check_password, password = True)
        else:
            password = check_field(check_password, args["password"])

        if args["prompt_otp"]:
            otp = check_prompt_field(check_otp, 'OTP: ')

        if args["https"]:
            protocol = "https://"
        else:
            protocol = "http://"

        # By default, Python attempts to perform certificate validation since v2.7.9.
        if args["ignore-cert"] and version_info[2] > 8:
            from functools import wraps
            from httplib import HTTPSConnection
            from ssl import _create_unverified_context

            old_init = HTTPSConnection.__init__

            @wraps(HTTPSConnection.__init__)
            def ignore_cert(self, *args, **kwargs):
                kwargs["context"] = _create_unverified_context()
                old_init(self, *args, **kwargs)

            HTTPSConnection.__init__ = ignore_cert

        directory = args["directory"]

        br = Browser()
        br.set_handle_robots(False)

        dt_format = "%Y-%m-%d_%H-%M-%S"
        dt = datetime.today()
        s = dt.strftime(dt_format)
        filename = address + "_" + s + ".xml"

        br.open(protocol + address + "/wp-login.php")
        br.select_form(name="loginform")
        br.form["log"] = user
        br.form["pwd"] = password

        if "otp" in locals():
            br.form["otp"] = otp

        br.submit()

        br.open(protocol + address + "/wp-admin/export.php")
        br.select_form(nr = 0)
        backup = br.submit()

        doctype_re = compile("<\!DOCTYPE html>$", MULTILINE)
        result = backup.read()

        # If the result is an HTML document instead of an XML document.
        if doctype_re.search(result):
            stderr.write("\n[x] Bad credentials\n")
            exit(1)

        with open(directory + "/" + filename, "w") as f:
            f.write(result)
    except KeyboardInterrupt:
        stdout.write("\n\n[*] Good Bye\n")
        exit(0)
    except ArgumentTypeError as e:
        stderr.write("\n[x] {0}\n".format(e))
        exit(1)
    except CertificateError as e:
        stderr.write("\n[x] {0}\n".format(e))
        stderr.write("[!] You can use the '--ignore-certificate' option to ignore this error\n")
        exit(1)
