"""passlib.handlers.misc - misc generic handlers
"""
#=============================================================================
# imports
#=============================================================================
# core
import sys
import logging; log = logging.getLogger(__name__)
from warnings import warn
# site
# pkg
from passlib.utils import to_native_str, str_consteq
from passlib.utils.compat import unicode, u, unicode_or_bytes_types
import passlib.utils.handlers as uh
# local
__all__ = [
    "unix_disabled",
    "unix_fallback",
    "plaintext",
]

#=============================================================================
# handler
#=============================================================================
class unix_fallback(uh.ifc.DisabledHash, uh.StaticHandler):
    """This class provides the fallback behavior for unix shadow files, and follows the :ref:`password-hash-api`.

    This class does not implement a hash, but instead provides fallback
    behavior as found in /etc/shadow on most unix variants.
    If used, should be the last scheme in the context.

    * this class will positively identify all hash strings.
    * for security, passwords will always hash to ``!``.
    * it rejects all passwords if the hash is NOT an empty string (``!`` or ``*`` are frequently used).
    * by default it rejects all passwords if the hash is an empty string,
      but if ``enable_wildcard=True`` is passed to verify(),
      all passwords will be allowed through if the hash is an empty string.

    .. deprecated:: 1.6
        This has been deprecated due to its "wildcard" feature,
        and will be removed in Passlib 1.8. Use :class:`unix_disabled` instead.
    """
    name = "unix_fallback"
    context_kwds = ("enable_wildcard",)

    @classmethod
    def identify(cls, hash):
        if isinstance(hash, unicode_or_bytes_types):
            return True
        else:
            raise uh.exc.ExpectedStringError(hash, "hash")

    def __init__(self, enable_wildcard=False, **kwds):
        warn("'unix_fallback' is deprecated, "
             "and will be removed in Passlib 1.8; "
             "please use 'unix_disabled' instead.",
             DeprecationWarning)
        super(unix_fallback, self).__init__(**kwds)
        self.enable_wildcard = enable_wildcard

    def _calc_checksum(self, secret):
        if self.checksum:
            # NOTE: hash will generally be "!", but we want to preserve
            # it in case it's something else, like "*".
            return self.checksum
        else:
            return u("!")

    @classmethod
    def verify(cls, secret, hash, enable_wildcard=False):
        uh.validate_secret(secret)
        if not isinstance(hash, unicode_or_bytes_types):
            raise uh.exc.ExpectedStringError(hash, "hash")
        elif hash:
            return False
        else:
            return enable_wildcard

_MARKER_CHARS = u("*!")
_MARKER_BYTES = b"*!"

class unix_disabled(uh.ifc.DisabledHash, uh.MinimalHandler):
    """This class provides disabled password behavior for unix shadow files,
    and follows the :ref:`password-hash-api`.

    This class does not implement a hash, but instead matches the "disabled account"
    strings found in ``/etc/shadow`` on most Unix variants. "encrypting" a password
    will simply return the disabled account marker. It will reject all passwords,
    no matter the hash string. The :meth:`~passlib.ifc.PasswordHash.hash`
    method supports one optional keyword:

    :type marker: str
    :param marker:
        Optional marker string which overrides the platform default
        used to indicate a disabled account.

        If not specified, this will default to ``"*"`` on BSD systems,
        and use the Linux default ``"!"`` for all other platforms.
        (:attr:`!unix_disabled.default_marker` will contain the default value)

    .. versionadded:: 1.6
        This class was added as a replacement for the now-deprecated
        :class:`unix_fallback` class, which had some undesirable features.
    """
    name = "unix_disabled"
    setting_kwds = ("marker",)
    context_kwds = ()

    _disable_prefixes = tuple(str(_MARKER_CHARS))

    # TODO: rename attr to 'marker'...
    if 'bsd' in sys.platform: # pragma: no cover -- runtime detection
        default_marker = u("*")
    else:
        # use the linux default for other systems
        # (glibc also supports adding old hash after the marker
        # so it can be restored later).
        default_marker = u("!")

    @classmethod
    def using(cls, marker=None, **kwds):
        subcls = super(unix_disabled, cls).using(**kwds)
        if marker is not None:
            if not cls.identify(marker):
                raise ValueError("invalid marker: %r" % marker)
            subcls.default_marker = marker
        return subcls

    @classmethod
    def identify(cls, hash):
        # NOTE: technically, anything in the /etc/shadow password field
        #       which isn't valid crypt() output counts as "disabled".
        #       but that's rather ambiguous, and it's hard to predict what
        #       valid output is for unknown crypt() implementations.
        #       so to be on the safe side, we only match things *known*
        #       to be disabled field indicators, and will add others
        #       as they are found. things beginning w/ "$" should *never* match.
        #
        # things currently matched:
        #       * linux uses "!"
        #       * bsd uses "*"
        #       * linux may use "!" + hash to disable but preserve original hash
        #       * linux counts empty string as "any password";
        #         this code recognizes it, but treats it the same as "!"
        if isinstance(hash, unicode):
            start = _MARKER_CHARS
        elif isinstance(hash, bytes):
            start = _MARKER_BYTES
        else:
            raise uh.exc.ExpectedStringError(hash, "hash")
        return not hash or hash[0] in start

    @classmethod
    def verify(cls, secret, hash):
        uh.validate_secret(secret)
        if not cls.identify(hash): # handles typecheck
            raise uh.exc.InvalidHashError(cls)
        return False

    @classmethod
    def hash(cls, secret, **kwds):
        if kwds:
            uh.warn_hash_settings_deprecation(cls, kwds)
            return cls.using(**kwds).hash(secret)
        uh.validate_secret(secret)
        marker = cls.default_marker
        assert marker and cls.identify(marker)
        return to_native_str(marker, param="marker")

    @uh.deprecated_method(deprecated="1.7", removed="2.0")
    @classmethod
    def genhash(cls, secret, config, marker=None):
        if not cls.identify(config):
            raise uh.exc.InvalidHashError(cls)
        elif config:
            # preserve the existing str,since it might contain a disabled password hash ("!" + hash)
            uh.validate_secret(secret)
            return to_native_str(config, param="config")
        else:
            if marker is not None:
                cls = cls.using(marker=marker)
            return cls.hash(secret)

    @classmethod
    def disable(cls, hash=None):
        out = cls.hash("")
        if hash is not None:
            hash = to_native_str(hash, param="hash")
            if cls.identify(hash):
                # extract original hash, so that we normalize marker
                hash = cls.enable(hash)
            if hash:
                out += hash
        return out

    @classmethod
    def enable(cls, hash):
        hash = to_native_str(hash, param="hash")
        for prefix in cls._disable_prefixes:
            if hash.startswith(prefix):
                orig = hash[len(prefix):]
                if orig:
                    return orig
                else:
                    raise ValueError("cannot restore original hash")
        raise uh.exc.InvalidHashError(cls)

class plaintext(uh.MinimalHandler):
    """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.

    The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the
    following additional contextual keyword:

    :type encoding: str
    :param encoding:
        This controls the character encoding to use (defaults to ``utf-8``).

        This encoding will be used to encode :class:`!unicode` passwords
        under Python 2, and decode :class:`!bytes` hashes under Python 3.

    .. versionchanged:: 1.6
        The ``encoding`` keyword was added.
    """
    # NOTE: this is subclassed by ldap_plaintext

    name = "plaintext"
    setting_kwds = ()
    context_kwds = ("encoding",)
    default_encoding = "utf-8"

    @classmethod
    def identify(cls, hash):
        if isinstance(hash, unicode_or_bytes_types):
            return True
        else:
            raise uh.exc.ExpectedStringError(hash, "hash")

    @classmethod
    def hash(cls, secret, encoding=None):
        uh.validate_secret(secret)
        if not encoding:
            encoding = cls.default_encoding
        return to_native_str(secret, encoding, "secret")

    @classmethod
    def verify(cls, secret, hash, encoding=None):
        if not encoding:
            encoding = cls.default_encoding
        hash = to_native_str(hash, encoding, "hash")
        if not cls.identify(hash):
            raise uh.exc.InvalidHashError(cls)
        return str_consteq(cls.hash(secret, encoding), hash)

    @uh.deprecated_method(deprecated="1.7", removed="2.0")
    @classmethod
    def genconfig(cls):
        return cls.hash("")

    @uh.deprecated_method(deprecated="1.7", removed="2.0")
    @classmethod
    def genhash(cls, secret, config, encoding=None):
        # NOTE: 'config' is ignored, as this hash has no salting / etc
        if not cls.identify(config):
            raise uh.exc.InvalidHashError(cls)
        return cls.hash(secret, encoding=encoding)

#=============================================================================
# eof
#=============================================================================
