<commit_before>from __future__ import unicode_literals

import logging
import uuid
from time import time

from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db import models
from django.db import IntegrityError
from django.utils import six, timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.http import urlquote
from django.utils.six.moves import range
from django.utils.translation import ugettext_lazy as _
from djblets.cache.backend import cache_memoize, make_cache_key
from djblets.db.fields import JSONField
from djblets.log import log_timed

from reviewboard.hostingsvcs.models import HostingServiceAccount
from reviewboard.scmtools.crypto_utils import (decrypt_password,
                                               encrypt_password)
from reviewboard.scmtools.managers import RepositoryManager, ToolManager
from reviewboard.scmtools.signals import (checked_file_exists,
                                          checking_file_exists,
                                          fetched_file, fetching_file)
from reviewboard.scmtools.core import FileNotFoundError
from reviewboard.site.models import LocalSite


@python_2_unicode_compatible
class Tool(models.Model):
    name = models.CharField(max_length=32, unique=True)
    class_name = models.CharField(max_length=128, unique=True)

    objects = ToolManager()

    # Templates can't access variables on a class properly. It'll attempt to
    # instantiate the class, which will fail without the necessary parameters.
    # So, we use these as convenient wrappers to do what the template can't do.
    supports_authentication = property(
        lambda x: x.scmtool_class.supports_authentication)
    supports_raw_file_urls = property(
        lambda x: x.scmtool_class.supports_raw_file_urls)
    supports_ticket_auth = property(
        lambda x: x.scmtool_class.supports_ticket_auth)
    supports_pending_changesets = property(
        lambda x: x.scmtool_class.supports_pending_changesets)
    field_help_text = property(
        lambda x: x.scmtool_class.field_help_text)

    def __str__(self):
        return self.name

    def get_scmtool_class(self):
        if not hasattr(self, '_scmtool_class'):
            path = self.class_name
            i = path.rfind('.')
            module, attr = path[:i], path[i + 1:]

            try:
                mod = __import__(six.binary_type(module), {}, {},
                                 [six.binary_type(attr)])
            except ImportError as e:
                raise ImproperlyConfigured(
                    'Error importing SCM Tool %s: "%s"' % (module, e))

            try:
                self._scmtool_class = getattr(mod, attr)
            except AttributeError:
                raise ImproperlyConfigured(
                    'Module "%s" does not define a "%s" SCM Tool'
                    % (module, attr))

        return self._scmtool_class
    scmtool_class = property(get_scmtool_class)

    class Meta:
        ordering = ("name",)


@python_2_unicode_compatible
class Repository(models.Model):
    ENCRYPTED_PASSWORD_PREFIX = '\t'

    name = models.CharField(max_length=64)
    path = models.CharField(max_length=255)
    mirror_path = models.CharField(max_length=255, blank=True)
    raw_file_url = models.CharField(
        _('Raw file URL mask'),
        max_length=255,
        blank=True,
        help_text=_("A URL mask used to check out a particular revision of a "
                    "file using HTTP. This is needed for repository types "
                    "that can't access remote files natively. "
                    "Use <tt>&lt;revision&gt;</tt> and "
                    "<tt>&lt;filename&gt;</tt> in the URL in place of the "
                    "revision and filename parts of the path."))
    username = models.CharField(max_length=32, blank=True)
    encrypted_password = models.CharField(max_length=128, blank=True,
                                          db_column='password')
    extra_data = JSONField(null=True)

    tool = models.ForeignKey(Tool, related_name="repositories")
    hosting_account = models.ForeignKey(
        HostingServiceAccount,
        related_name='repositories',
        verbose_name=_('Hosting service account'),
        blank=True,
        null=True)

    bug_tracker = models.CharField(
        _('Bug tracker URL'),
        max_length=256,
        blank=True,
        help_text=_("This should be the full path to a bug in the bug tracker "
                    "for this repository, using '%s' in place of the bug ID."))
    encoding = models.CharField(
        max_length=32,
        blank=True,
        help_text=_("The encoding used for files in this repository. This is "
                    "an advanced setting and should only be used if you're "
                    "sure you need it."))
    visible = models.BooleanField(
        _('Show this repository'),
        default=True,
        help_text=_('Use this to control whether or not a repository is '
                    'shown when creating new review requests. Existing '
                    'review requests are unaffected.'))

    archived = models.BooleanField(
        _('Archived'),
        default=False,
        help_text=_("Archived repositories do not show up in lists of "
                    "repositories, and aren't open to new review requests."))

    archived_timestamp = models.DateTimeField(null=True, blank=True)

    # Access control
    local_site = models.ForeignKey(LocalSite,
                                   verbose_name=_('Local site'),
                                   blank=True,
                                   null=True)
    public = models.BooleanField(
        _('publicly accessible'),
        default=True,
        help_text=_('Review requests and files on public repositories are '
                    'visible to anyone. Private repositories must explicitly '
                    'list the users and groups that can access them.'))

    users = models.ManyToManyField(
        User,
        limit_choices_to={'is_active': True},
        blank=True,
        related_name='repositories',
        verbose_name=_('Users with access'),
        help_text=_('A list of users with explicit access to the repository.'))
    review_groups = models.ManyToManyField(
        'reviews.Group',
        limit_choices_to={'invite_only': True},
        blank=True,
        related_name='repositories',
        verbose_name=_('Review groups with access'),
        help_text=_('A list of invite-only review groups whose members have '
                    'explicit access to the repository.'))

    hooks_uuid = models.CharField(
        _('Hooks UUID'),
        max_length=32,
        null=True,
        blank=True,
        help_text=_('Unique identifier used for validating incoming '
                    'webhooks.'))

    objects = RepositoryManager()

    BRANCHES_CACHE_PERIOD = 60 * 5  # 5 minutes
    COMMITS_CACHE_PERIOD_SHORT = 60 * 5  # 5 minutes
    COMMITS_CACHE_PERIOD_LONG = 60 * 60 * 24  # 1 day

    def _set_password(self, value):
        """Sets the password for the repository.

        The password will be stored as an encrypted value, prefixed with a
        tab character in order to differentiate between legacy plain-text
        passwords.
        """
        if value:
            value = encrypt_password(value)

        self.encrypted_password = <PASSWORD>' % (self.ENCRYPTED_PASSWORD_PREFIX,
                                            value)

    def _get_password(self):
        """Returns the password for the repository.

        If a password is stored and encrypted, it will be decrypted and
        returned.

        If the stored password is in plain-text, then it will be encrypted,
        stored in the database, and returned.
        """
        password = <PASSWORD>

        if not password:
            password = None
        elif password.startswith(self.ENCRYPTED_PASSWORD_PREFIX):
            password = <PASSWORD>password(
                password[len(self.ENCRYPTED_PASSWORD_PREFIX):])
        else:
            # This is a plain-text password. Convert it.
            self.password = password
            self.save(update_fields=['encrypted_password'])

        return password

    password = property(_get_password, _set_password)

    def get_scmtool(self):
        cls = self.tool.get_scmtool_class()
        return cls(self)

    @property
    def hosting_service(self):
        if self.hosting_account:
            return self.hosting_account.service<commit_msg>Fix problems in hardened password storage.<commit_after>from __future__ import unicode_literals

import logging
import uuid
from time import time

from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db import models
from django.db import IntegrityError
from django.utils import six, timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.http import urlquote
from django.utils.six.moves import range
from django.utils.translation import ugettext_lazy as _
from djblets.cache.backend import cache_memoize, make_cache_key
from djblets.db.fields import JSONField
from djblets.log import log_timed

from reviewboard.hostingsvcs.models import HostingServiceAccount
from reviewboard.scmtools.crypto_utils import (decrypt_password,
                                               encrypt_password)
from reviewboard.scmtools.managers import RepositoryManager, ToolManager
from reviewboard.scmtools.signals import (checked_file_exists,
                                          checking_file_exists,
                                          fetched_file, fetching_file)
from reviewboard.scmtools.core import FileNotFoundError
from reviewboard.site.models import LocalSite


@python_2_unicode_compatible
class Tool(models.Model):
    name = models.CharField(max_length=32, unique=True)
    class_name = models.CharField(max_length=128, unique=True)

    objects = ToolManager()

    # Templates can't access variables on a class properly. It'll attempt to
    # instantiate the class, which will fail without the necessary parameters.
    # So, we use these as convenient wrappers to do what the template can't do.
    supports_authentication = property(
        lambda x: x.scmtool_class.supports_authentication)
    supports_raw_file_urls = property(
        lambda x: x.scmtool_class.supports_raw_file_urls)
    supports_ticket_auth = property(
        lambda x: x.scmtool_class.supports_ticket_auth)
    supports_pending_changesets = property(
        lambda x: x.scmtool_class.supports_pending_changesets)
    field_help_text = property(
        lambda x: x.scmtool_class.field_help_text)

    def __str__(self):
        return self.name

    def get_scmtool_class(self):
        if not hasattr(self, '_scmtool_class'):
            path = self.class_name
            i = path.rfind('.')
            module, attr = path[:i], path[i + 1:]

            try:
                mod = __import__(six.binary_type(module), {}, {},
                                 [six.binary_type(attr)])
            except ImportError as e:
                raise ImproperlyConfigured(
                    'Error importing SCM Tool %s: "%s"' % (module, e))

            try:
                self._scmtool_class = getattr(mod, attr)
            except AttributeError:
                raise ImproperlyConfigured(
                    'Module "%s" does not define a "%s" SCM Tool'
                    % (module, attr))

        return self._scmtool_class
    scmtool_class = property(get_scmtool_class)

    class Meta:
        ordering = ("name",)


@python_2_unicode_compatible
class Repository(models.Model):
    ENCRYPTED_PASSWORD_PREFIX = '\t'

    name = models.CharField(max_length=64)
    path = models.CharField(max_length=255)
    mirror_path = models.CharField(max_length=255, blank=True)
    raw_file_url = models.CharField(
        _('Raw file URL mask'),
        max_length=255,
        blank=True,
        help_text=_("A URL mask used to check out a particular revision of a "
                    "file using HTTP. This is needed for repository types "
                    "that can't access remote files natively. "
                    "Use <tt>&lt;revision&gt;</tt> and "
                    "<tt>&lt;filename&gt;</tt> in the URL in place of the "
                    "revision and filename parts of the path."))
    username = models.CharField(max_length=32, blank=True)
    encrypted_password = models.CharField(max_length=128, blank=True,
                                          db_column='password')
    extra_data = JSONField(null=True)

    tool = models.ForeignKey(Tool, related_name="repositories")
    hosting_account = models.ForeignKey(
        HostingServiceAccount,
        related_name='repositories',
        verbose_name=_('Hosting service account'),
        blank=True,
        null=True)

    bug_tracker = models.CharField(
        _('Bug tracker URL'),
        max_length=256,
        blank=True,
        help_text=_("This should be the full path to a bug in the bug tracker "
                    "for this repository, using '%s' in place of the bug ID."))
    encoding = models.CharField(
        max_length=32,
        blank=True,
        help_text=_("The encoding used for files in this repository. This is "
                    "an advanced setting and should only be used if you're "
                    "sure you need it."))
    visible = models.BooleanField(
        _('Show this repository'),
        default=True,
        help_text=_('Use this to control whether or not a repository is '
                    'shown when creating new review requests. Existing '
                    'review requests are unaffected.'))

    archived = models.BooleanField(
        _('Archived'),
        default=False,
        help_text=_("Archived repositories do not show up in lists of "
                    "repositories, and aren't open to new review requests."))

    archived_timestamp = models.DateTimeField(null=True, blank=True)

    # Access control
    local_site = models.ForeignKey(LocalSite,
                                   verbose_name=_('Local site'),
                                   blank=True,
                                   null=True)
    public = models.BooleanField(
        _('publicly accessible'),
        default=True,
        help_text=_('Review requests and files on public repositories are '
                    'visible to anyone. Private repositories must explicitly '
                    'list the users and groups that can access them.'))

    users = models.ManyToManyField(
        User,
        limit_choices_to={'is_active': True},
        blank=True,
        related_name='repositories',
        verbose_name=_('Users with access'),
        help_text=_('A list of users with explicit access to the repository.'))
    review_groups = models.ManyToManyField(
        'reviews.Group',
        limit_choices_to={'invite_only': True},
        blank=True,
        related_name='repositories',
        verbose_name=_('Review groups with access'),
        help_text=_('A list of invite-only review groups whose members have '
                    'explicit access to the repository.'))

    hooks_uuid = models.CharField(
        _('Hooks UUID'),
        max_length=32,
        null=True,
        blank=True,
        help_text=_('Unique identifier used for validating incoming '
                    'webhooks.'))

    objects = RepositoryManager()

    BRANCHES_CACHE_PERIOD = 60 * 5  # 5 minutes
    COMMITS_CACHE_PERIOD_SHORT = 60 * 5  # 5 minutes
    COMMITS_CACHE_PERIOD_LONG = 60 * 60 * 24  # 1 day

    def _set_password(self, value):
        """Sets the password for the repository.

        The password will be stored as an encrypted value, prefixed with a
        tab character in order to differentiate between legacy plain-text
        passwords.
        """
        if value:
            value = '%s%s' % (self.ENCRYPTED_PASSWORD_PREFIX,
                              encrypt_password(value))
        else:
            value = ''

        self.encrypted_password = value

    def _get_password(self):
        """Returns the password for the repository.

        If a password is stored and encrypted, it will be decrypted and
        returned.

        If the stored password is in plain-text, then it will be encrypted,
        stored in the database, and returned.
        """
        password = <PASSWORD>

        # NOTE: Due to a bug in 2.0.9, it was possible to get a string of
        #       "\tNone", indicating no password. We have to check for this.
        if not password or password == <PASSWORD>':
            password = <PASSWORD>
        elif password.startswith(self.ENCRYPTED_PASSWORD_PREFIX):
            password = password[len(self.ENCRYPTED_PASSWORD_PREFIX):]

            if password:
                password = decrypt_password(password)
            else:
                password = None
        else:
            # This is a plain-text password. Convert it.
            self.password = password
            self.save(update_fields=['encrypted_password'])

        return password

    password = property(_get_password, _set_password)

    def get_scmtool(self):
        cls = self.tool.get_scmtool_class()
        return cls(self)

    @property
    def hosting_service(self):
        if self.hosting_account:
            return self.hosting_account.service
{'fraction_non_alphanumeric': 0.07776517300056722, 'fraction_numerical': 0.005501985252410664, 'mean_word_length': 2.2807964272422776, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 26, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7125280', 'n_tokens_mistral': 4879, 'n_tokens_neox': 4645, 'n_words': 1362}