示例#1
0
def disable_addon_for_block(block):
    """Disable appropriate addon versions that are affected by the Block, and
    the addon too if 0 - *."""
    from .models import Block
    from olympia.reviewers.utils import ReviewBase

    review = ReviewBase(
        request=None,
        addon=block.addon,
        version=None,
        review_type='pending',
        user=get_task_user(),
    )
    review.set_data({
        'versions': [
            ver for ver in block.addon_versions
            # We don't need to reject versions from older deleted instances
            if ver.addon == block.addon
            and block.is_version_blocked(ver.version)
        ]
    })
    review.reject_multiple_versions()

    for version in review.data['versions']:
        # Clear needs_human_review on rejected versions, we consider that
        # the admin looked at them before blocking.
        review.clear_specific_needs_human_review_flags(version)

    if block.min_version == Block.MIN and block.max_version == Block.MAX:
        if block.addon.status == amo.STATUS_DELETED:
            block.addon.deny_resubmission()
        else:
            block.addon.update(status=amo.STATUS_DISABLED)
示例#2
0
def log_and_notify(action, comments, note_creator, version):
    log_kwargs = {
        'user': note_creator,
        'created': datetime.datetime.now(),
    }
    if comments:
        log_kwargs['details'] = {
            'comments': comments,
            'version': version.version}
    else:
        # Just use the name of the action if no comments provided.  Alas we
        # can't know the locale of recipient, and our templates are English
        # only so prevent language jumble by forcing into en-US.
        with no_translation():
            comments = '%s' % action.short
    note = amo.log(action, version.addon, version, **log_kwargs)

    # Collect reviewers involved with this version.
    review_perm = ('Review' if version.channel == amo.RELEASE_CHANNEL_LISTED
                   else 'ReviewUnlisted')
    log_users = {
        alog.user for alog in ActivityLog.objects.for_version(version) if
        acl.action_allowed_user(alog.user, 'Addons', review_perm)}
    # Collect add-on authors (excl. the person who sent the email.)
    addon_authors = set(version.addon.authors.all()) - {note_creator}
    # Collect staff that want a copy of the email
    staff_cc = set(
        UserProfile.objects.filter(groups__name=ACTIVITY_MAIL_GROUP))
    # If task_user doesn't exist that's no big issue (i.e. in tests)
    try:
        task_user = {get_task_user()}
    except UserProfile.DoesNotExist:
        task_user = set()
    # Collect reviewers on the thread (excl. the email sender and task user for
    # automated messages).
    reviewers = ((log_users | staff_cc) - addon_authors - task_user -
                 {note_creator})
    author_context_dict = {
        'name': version.addon.name,
        'number': version.version,
        'author': note_creator.name,
        'comments': comments,
        'url': absolutify(version.addon.get_dev_url('versions')),
        'SITE_URL': settings.SITE_URL,
    }
    reviewer_context_dict = author_context_dict.copy()
    reviewer_context_dict['url'] = absolutify(
        reverse('editors.review', args=[version.addon.pk], add_prefix=False))

    # Not being localised because we don't know the recipients locale.
    subject = 'Mozilla Add-ons: %s Updated' % version.addon.name
    template = loader.get_template('activity/emails/developer.txt')
    send_activity_mail(
        subject, template.render(Context(author_context_dict)), version,
        addon_authors, settings.EDITORS_EMAIL)
    send_activity_mail(
        subject, template.render(Context(reviewer_context_dict)), version,
        reviewers, settings.EDITORS_EMAIL)
    return note
示例#3
0
 def wrapper(*args, **kw):
     old_user = get_user()
     set_user(get_task_user())
     try:
         result = f(*args, **kw)
     finally:
         set_user(old_user)
     return result
示例#4
0
def delete_imported_block_from_blocklist(kinto_id):
    existing_blocks = (Block.objects.filter(kinto_id__in=(kinto_id,
                                                          f'*{kinto_id}')))
    task_user = get_task_user()
    for block in existing_blocks:
        block_activity_log_delete(block, delete_user=task_user)
        block.delete()
    KintoImport.objects.get(kinto_id=kinto_id).delete()
示例#5
0
 def wrapper(*args, **kw):
     old_user = get_user()
     set_user(get_task_user())
     try:
         result = f(*args, **kw)
     finally:
         set_user(old_user)
     return result
示例#6
0
def delete_imported_block_from_blocklist(legacy_id):
    existing_blocks = Block.objects.filter(legacy_id__in=(legacy_id,
                                                          f'*{legacy_id}'))
    task_user = get_task_user()
    for block in existing_blocks:
        block_activity_log_delete(block, delete_user=task_user)
        block.delete()
        statsd.incr('blocklist.tasks.import_blocklist.block_deleted')
    LegacyImport.objects.get(legacy_id=legacy_id).delete()
    statsd.incr('blocklist.tasks.import_blocklist.deleted_record_processed')
示例#7
0
def log_and_notify(action, comments, note_creator, version):
    log_kwargs = {
        'user': note_creator,
        'created': datetime.datetime.now(),
        'details': {
            'comments': comments,
            'version': version.version
        }
    }
    note = amo.log(action, version.addon, version, **log_kwargs)

    # Collect reviewers involved with this version.
    review_perm = ('Review' if version.channel == amo.RELEASE_CHANNEL_LISTED
                   else 'ReviewUnlisted')
    log_users = {
        alog.user
        for alog in ActivityLog.objects.for_version(version)
        if acl.action_allowed_user(alog.user, 'Addons', review_perm)
    }
    # Collect add-on authors (excl. the person who sent the email.)
    addon_authors = set(version.addon.authors.all()) - {note_creator}
    # Collect staff that want a copy of the email
    staff_cc = set(
        UserProfile.objects.filter(groups__name=ACTIVITY_MAIL_GROUP))
    # If task_user doesn't exist that's no big issue (i.e. in tests)
    try:
        task_user = {get_task_user()}
    except UserProfile.DoesNotExist:
        task_user = set()
    # Collect reviewers on the thread (excl. the email sender and task user for
    # automated messages).
    reviewers = ((log_users | staff_cc) - addon_authors - task_user -
                 {note_creator})
    author_context_dict = {
        'name': version.addon.name,
        'number': version.version,
        'author': note_creator.name,
        'comments': comments,
        'url': version.addon.get_dev_url('versions'),
        'SITE_URL': settings.SITE_URL,
    }
    reviewer_context_dict = author_context_dict.copy()
    reviewer_context_dict['url'] = absolutify(
        reverse('editors.review', args=[version.addon.pk], add_prefix=False))

    # Not being localised because we don't know the recipients locale.
    subject = 'Mozilla Add-ons: %s Updated' % version.addon.name
    template = loader.get_template('activity/emails/developer.txt')
    send_activity_mail(subject, template.render(Context(author_context_dict)),
                       version, addon_authors, settings.EDITORS_EMAIL)
    send_activity_mail(subject,
                       template.render(Context(reviewer_context_dict)),
                       version, reviewers, settings.EDITORS_EMAIL)
    return note
示例#8
0
def import_block_from_blocklist(record):
    log.debug('Processing block id: [%s]', record.get('id'))
    guid = record.get('guid')
    if not guid:
        log.error('GUID is falsey, skipping.')
        return
    version_range = record.get('versionRange', [{}])[0]
    target_application = version_range.get('targetApplication') or [{}]
    target_GUID = target_application[0].get('guid')
    if target_GUID and target_GUID != amo.FIREFOX.guid:
        log.error('targetApplication (%s) is not Firefox, skipping.',
                  target_GUID)
        return
    block_kw = {
        'min_version': version_range.get('minVersion', '0'),
        'max_version': version_range.get('maxVersion', '*'),
        'url': record.get('details', {}).get('bug'),
        'reason': record.get('details', {}).get('why', ''),
        'kinto_id': record.get('id'),
        'include_in_legacy': True,
        'updated_by': get_task_user(),
    }
    modified_date = datetime.fromtimestamp(
        record.get('last_modified',
                   time.time() * 1000) / 1000)

    if guid.startswith('/'):
        # need to escape the {} brackets or mysql chokes.
        guid_regexp = bracket_open_regex.sub(r'\{', guid[1:-1])
        guid_regexp = bracket_close_regex.sub(r'\}', guid_regexp)
        log.debug('Attempting to create Blocks for addons matching [%s]',
                  guid_regexp)
        addons_qs = Addon.unfiltered.filter(guid__regex=guid_regexp)
        # We need to mark this id in a way so we know its from a
        # regex guid - otherwise we might accidentally overwrite it.
        block_kw['kinto_id'] = '*' + block_kw['kinto_id']
    else:
        log.debug('Attempting to create a Block for guid [%s]', guid)
        addons_qs = Addon.unfiltered.filter(guid=guid)
    for addon in addons_qs:
        (block, created) = Block.objects.update_or_create(guid=addon.guid,
                                                          defaults=dict(
                                                              guid=addon.guid,
                                                              **block_kw))
        block_activity_log_save(block, change=not created)
        if created:
            log.debug('Added Block for [%s]', block.guid)
            block.update(modified=modified_date)
        else:
            log.debug('Updated Block for [%s]', block.guid)
    else:
        log.debug('No addon found for block id: [%s]', record.get('id'))
示例#9
0
def update_maxversions(version_pks, job_pk, data, **kw):
    log.info('[%s@%s] Updating max version for job %s.' %
             (len(version_pks), update_maxversions.rate_limit, job_pk))
    job = ValidationJob.objects.get(pk=job_pk)
    set_user(get_task_user())
    dry_run = data['preview_only']
    app_id = job.target_version.application
    stats = collections.defaultdict(int)
    stats['processed'] = 0
    stats['is_dry_run'] = int(dry_run)
    for version in Version.objects.filter(pk__in=version_pks):
        stats['processed'] += 1
        file_pks = version.files.values_list('pk', flat=True)
        errors = (ValidationResult.objects.filter(
            validation_job=job, file__pk__in=file_pks).values_list('errors',
                                                                   flat=True))
        if any(errors):
            stats['invalid'] += 1
            log.info('Version %s for addon %s not updated, '
                     'one of the files did not pass validation' %
                     (version.pk, version.addon.pk))
            continue

        for app in version.apps.filter(
                application=job.curr_max_version.application,
                max__version_int__gte=job.curr_max_version.version_int,
                max__version_int__lt=job.target_version.version_int):
            stats['bumped'] += 1
            log.info(
                'Updating version %s%s for addon %s from version %s '
                'to version %s' %
                (version.pk, ' [DRY RUN]' if dry_run else '', version.addon.pk,
                 job.curr_max_version.version, job.target_version.version))
            app.max = job.target_version
            if not dry_run:
                app.save()
                amo.log(amo.LOG.MAX_APPVERSION_UPDATED,
                        version.addon,
                        version,
                        details={
                            'version': version.version,
                            'target': job.target_version.version,
                            'application': app_id
                        })

    log.info('[%s@%s] bulk update stats for job %s: {%s}' %
             (len(version_pks), update_maxversions.rate_limit, job_pk,
              ', '.join('%s: %s' % (k, stats[k])
                        for k in sorted(stats.keys()))))
示例#10
0
def update_maxversions(version_pks, job_pk, data, **kw):
    log.info('[%s@%s] Updating max version for job %s.'
             % (len(version_pks), update_maxversions.rate_limit, job_pk))
    job = ValidationJob.objects.get(pk=job_pk)
    set_user(get_task_user())
    dry_run = data['preview_only']
    app_id = job.target_version.application
    stats = collections.defaultdict(int)
    stats['processed'] = 0
    stats['is_dry_run'] = int(dry_run)
    for version in Version.objects.filter(pk__in=version_pks):
        stats['processed'] += 1
        file_pks = version.files.values_list('pk', flat=True)
        errors = (ValidationResult.objects.filter(validation_job=job,
                                                  file__pk__in=file_pks)
                                          .values_list('errors', flat=True))
        if any(errors):
            stats['invalid'] += 1
            log.info('Version %s for addon %s not updated, '
                     'one of the files did not pass validation'
                     % (version.pk, version.addon.pk))
            continue

        for app in version.apps.filter(
                application=job.curr_max_version.application,
                max__version_int__gte=job.curr_max_version.version_int,
                max__version_int__lt=job.target_version.version_int):
            stats['bumped'] += 1
            log.info('Updating version %s%s for addon %s from version %s '
                     'to version %s'
                     % (version.pk,
                        ' [DRY RUN]' if dry_run else '',
                        version.addon.pk,
                        job.curr_max_version.version,
                        job.target_version.version))
            app.max = job.target_version
            if not dry_run:
                app.save()
                amo.log(amo.LOG.MAX_APPVERSION_UPDATED,
                        version.addon, version,
                        details={'version': version.version,
                                 'target': job.target_version.version,
                                 'application': app_id})

    log.info('[%s@%s] bulk update stats for job %s: {%s}'
             % (len(version_pks), update_maxversions.rate_limit, job_pk,
                ', '.join('%s: %s' % (k, stats[k])
                          for k in sorted(stats.keys()))))
示例#11
0
 def __init__(self, request, addon, version, review_type,
              content_review=False, user=None):
     self.request = request
     if request:
         self.user = user or self.request.user
         self.human_review = True
     else:
         # Use the addons team go-to user "Mozilla" for the automatic
         # validations.
         self.user = user or get_task_user()
         self.human_review = False
     self.addon = addon
     self.version = version
     self.review_type = (
         ('theme_%s' if addon.type == amo.ADDON_STATICTHEME
          else 'extension_%s') % review_type)
     self.files = self.version.unreviewed_files if self.version else []
     self.content_review = content_review
     self.redirect_url = None
示例#12
0
    def handle(self, *args, **options):
        with open(options.get('guids_input'), 'r') as guid_file:
            input_guids = guid_file.read()
        guids = splitlines(input_guids)

        block_args = {
            prop: options.get(prop)
            for prop in ('min_version', 'max_version', 'reason', 'url')
            if options.get(prop)
        }
        block_args['updated_by'] = get_task_user()
        block_args['include_in_legacy'] = False
        submission = BlocklistSubmission(**block_args)

        for guids_chunk in chunked(guids, 100):
            blocks = save_guids_to_blocks(guids_chunk, submission)
            print(
                f'Added/Updated {len(blocks)} blocks from {len(guids_chunk)} '
                'guids')
示例#13
0
def notify_about_activity_log(addon,
                              version,
                              note,
                              perm_setting=None,
                              send_to_reviewers=True,
                              send_to_staff=True):
    """Notify relevant users about an ActivityLog note."""
    comments = (note.details or {}).get('comments')
    if not comments:
        # Just use the name of the action if no comments provided.  Alas we
        # can't know the locale of recipient, and our templates are English
        # only so prevent language jumble by forcing into en-US.
        with translation.override(settings.LANGUAGE_CODE):
            comments = '%s' % amo.LOG_BY_ID[note.action].short
    else:
        htmlparser = HTMLParser()
        comments = htmlparser.unescape(comments)

    # Collect add-on authors (excl. the person who sent the email.) and build
    # the context for them.
    addon_authors = set(addon.authors.all()) - {note.user}

    author_context_dict = {
        'name': addon.name,
        'number': version.version,
        'author': note.user.name,
        'comments': comments,
        'url': absolutify(addon.get_dev_url('versions')),
        'SITE_URL': settings.SITE_URL,
        'email_reason': 'you are listed as an author of this add-on',
        'is_info_request': note.action == amo.LOG.REQUEST_INFORMATION.id,
    }

    # Not being localised because we don't know the recipients locale.
    with translation.override('en-US'):
        if note.action == amo.LOG.REQUEST_INFORMATION.id:
            if addon.pending_info_request:
                days_left = (
                    # We pad the time left with an extra hour so that the email
                    # does not end up saying "6 days left" because a few
                    # seconds or minutes passed between the datetime was saved
                    # and the email was sent.
                    addon.pending_info_request + timedelta(hours=1) -
                    datetime.now()).days
                if days_left > 9:
                    author_context_dict['number_of_days_left'] = ('%d days' %
                                                                  days_left)
                elif days_left > 1:
                    author_context_dict['number_of_days_left'] = (
                        '%s (%d) days' % (apnumber(days_left), days_left))
                else:
                    author_context_dict['number_of_days_left'] = 'one (1) day'
            subject = u'Mozilla Add-ons: Action Required for %s %s' % (
                addon.name, version.version)
            reviewer_subject = u'Mozilla Add-ons: %s %s' % (addon.name,
                                                            version.version)
        else:
            subject = reviewer_subject = u'Mozilla Add-ons: %s %s' % (
                addon.name, version.version)
    # Build and send the mail for authors.
    template = template_from_user(note.user, version)
    from_email = formataddr((note.user.name, NOTIFICATIONS_FROM_EMAIL))
    send_activity_mail(subject, template.render(author_context_dict), version,
                       addon_authors, from_email, note.id, perm_setting)

    if send_to_reviewers or send_to_staff:
        # If task_user doesn't exist that's no big issue (i.e. in tests)
        try:
            task_user = {get_task_user()}
        except UserProfile.DoesNotExist:
            task_user = set()

    if send_to_reviewers:
        # Collect reviewers on the thread (excl. the email sender and task user
        # for automated messages), build the context for them and send them
        # their copy.
        log_users = {
            alog.user
            for alog in ActivityLog.objects.for_version(version)
            if acl.is_user_any_kind_of_reviewer(alog.user)
        }
        reviewers = log_users - addon_authors - task_user - {note.user}
        reviewer_context_dict = author_context_dict.copy()
        reviewer_context_dict['url'] = absolutify(
            reverse('reviewers.review',
                    kwargs={
                        'addon_id': version.addon.pk,
                        'channel': amo.CHANNEL_CHOICES_API[version.channel]
                    },
                    add_prefix=False))
        reviewer_context_dict['email_reason'] = 'you reviewed this add-on'
        send_activity_mail(reviewer_subject,
                           template.render(reviewer_context_dict), version,
                           reviewers, from_email, note.id, perm_setting)

    if send_to_staff:
        # Collect staff that want a copy of the email, build the context for
        # them and send them their copy.
        staff = set(
            UserProfile.objects.filter(groups__name=ACTIVITY_MAIL_GROUP))
        staff_cc = (staff - reviewers - addon_authors - task_user -
                    {note.user})
        staff_cc_context_dict = reviewer_context_dict.copy()
        staff_cc_context_dict['email_reason'] = (
            'you are member of the activity email cc group')
        send_activity_mail(reviewer_subject,
                           template.render(staff_cc_context_dict), version,
                           staff_cc, from_email, note.id, perm_setting)
示例#14
0
    def from_upload(cls, upload, addon, selected_apps, channel,
                    parsed_data=None):
        """
        Create a Version instance and corresponding File(s) from a
        FileUpload, an Addon, a list of compatible app ids, a channel id and
        the parsed_data generated by parse_addon().

        Note that it's the caller's responsability to ensure the file is valid.
        We can't check for that here because an admin may have overridden the
        validation results.
        """
        from olympia.git.utils import create_git_extraction_entry

        assert parsed_data is not None

        if addon.status == amo.STATUS_DISABLED:
            raise VersionCreateError(
                'Addon is Mozilla Disabled; no new versions are allowed.')

        license_id = None
        if channel == amo.RELEASE_CHANNEL_LISTED:
            previous_version = addon.find_latest_version(
                channel=channel, exclude=())
            if previous_version and previous_version.license_id:
                license_id = previous_version.license_id
        approval_notes = None
        if parsed_data.get('is_mozilla_signed_extension'):
            approval_notes = (u'This version has been signed with '
                              u'Mozilla internal certificate.')
        version = cls.objects.create(
            addon=addon,
            approval_notes=approval_notes,
            version=parsed_data['version'],
            license_id=license_id,
            channel=channel,
        )
        email = upload.user.email if upload.user and upload.user.email else ''
        with core.override_remote_addr(upload.ip_address):
            log.info(
                'New version: %r (%s) from %r' % (version, version.id, upload),
                extra={
                    'email': email,
                    'guid': addon.guid,
                    'upload': upload.uuid.hex,
                    'user_id': upload.user_id,
                    'from_api': upload.source == amo.UPLOAD_SOURCE_API,
                }
            )
            activity.log_create(
                amo.LOG.ADD_VERSION, version, addon,
                user=upload.user or get_task_user())

        if addon.type == amo.ADDON_STATICTHEME:
            # We don't let developers select apps for static themes
            selected_apps = [app.id for app in amo.APP_USAGE]

        compatible_apps = {}
        for app in parsed_data.get('apps', []):
            if app.id not in selected_apps:
                # If the user chose to explicitly deselect Firefox for Android
                # we're not creating the respective `ApplicationsVersions`
                # which will have this add-on then be listed only for
                # Firefox specifically.
                continue

            compatible_apps[app.appdata] = ApplicationsVersions(
                version=version, min=app.min, max=app.max, application=app.id)
            compatible_apps[app.appdata].save()

        # See #2828: sometimes when we generate the filename(s) below, in
        # File.from_upload(), cache-machine is confused and has trouble
        # fetching the ApplicationsVersions that were just created. To work
        # around this we pre-generate version.compatible_apps and avoid the
        # queries completely.
        version._compatible_apps = compatible_apps

        # For backwards compatibility. We removed specific platform
        # support during submission but we don't handle it any different
        # beyond that yet. That means, we're going to simply set it
        # to `PLATFORM_ALL` and also have the backend create separate
        # files for each platform. Cleaning that up is another step.
        # Given the timing on this, we don't care about updates to legacy
        # add-ons as well.
        # Create relevant file and update the all_files cached property on the
        # Version, because we might need it afterwards.
        version.all_files = [File.from_upload(
            upload=upload, version=version, platform=amo.PLATFORM_ALL.id,
            parsed_data=parsed_data
        )]

        version.inherit_nomination(from_statuses=[amo.STATUS_AWAITING_REVIEW])
        version.disable_old_files()

        # After the upload has been copied to all platforms, remove the upload.
        storage.delete(upload.path)
        upload.path = ''
        upload.save()

        version_uploaded.send(instance=version, sender=Version)

        if version.is_webextension:
            if (
                    waffle.switch_is_active('enable-yara') or
                    waffle.switch_is_active('enable-customs') or
                    waffle.switch_is_active('enable-wat')
            ):
                ScannerResult.objects.filter(upload_id=upload.id).update(
                    version=version)

        if waffle.switch_is_active('enable-uploads-commit-to-git-storage'):
            # Schedule this version for git extraction.
            transaction.on_commit(
                lambda: create_git_extraction_entry(version=version)
            )

        # Generate a preview and icon for listed static themes
        if (addon.type == amo.ADDON_STATICTHEME and
                channel == amo.RELEASE_CHANNEL_LISTED):
            theme_data = parsed_data.get('theme', {})
            generate_static_theme_preview(theme_data, version.pk)

        # Authors need to be notified about auto-approval delay again since
        # they are submitting a new version.
        addon.reset_notified_about_auto_approval_delay()

        # Track the time it took from first upload through validation
        # (and whatever else) until a version was created.
        upload_start = utc_millesecs_from_epoch(upload.created)
        now = datetime.datetime.now()
        now_ts = utc_millesecs_from_epoch(now)
        upload_time = now_ts - upload_start

        log.info('Time for version {version} creation from upload: {delta}; '
                 'created={created}; now={now}'
                 .format(delta=upload_time, version=version,
                         created=upload.created, now=now))
        statsd.timing('devhub.version_created_from_upload', upload_time)

        return version
示例#15
0
def log_and_notify(action, comments, note_creator, version, perm_setting=None,
                   detail_kwargs=None):
    log_kwargs = {
        'user': note_creator,
        'created': datetime.datetime.now(),
    }
    if detail_kwargs is None:
        detail_kwargs = {}
    if comments:
        detail_kwargs['version'] = version.version
        detail_kwargs['comments'] = comments
    else:
        # Just use the name of the action if no comments provided.  Alas we
        # can't know the locale of recipient, and our templates are English
        # only so prevent language jumble by forcing into en-US.
        with no_translation():
            comments = '%s' % action.short
    if detail_kwargs:
        log_kwargs['details'] = detail_kwargs

    note = ActivityLog.create(action, version.addon, version, **log_kwargs)
    if not note:
        return

    # Collect reviewers involved with this version.
    review_perm = (amo.permissions.ADDONS_REVIEW
                   if version.channel == amo.RELEASE_CHANNEL_LISTED
                   else amo.permissions.ADDONS_REVIEW_UNLISTED)
    log_users = {
        alog.user for alog in ActivityLog.objects.for_version(version) if
        acl.action_allowed_user(alog.user, review_perm)}
    # Collect add-on authors (excl. the person who sent the email.)
    addon_authors = set(version.addon.authors.all()) - {note_creator}
    # Collect staff that want a copy of the email
    staff = set(
        UserProfile.objects.filter(groups__name=ACTIVITY_MAIL_GROUP))
    # If task_user doesn't exist that's no big issue (i.e. in tests)
    try:
        task_user = {get_task_user()}
    except UserProfile.DoesNotExist:
        task_user = set()
    # Collect reviewers on the thread (excl. the email sender and task user for
    # automated messages).
    reviewers = log_users - addon_authors - task_user - {note_creator}
    staff_cc = staff - reviewers - addon_authors - task_user - {note_creator}
    author_context_dict = {
        'name': version.addon.name,
        'number': version.version,
        'author': note_creator.name,
        'comments': comments,
        'url': absolutify(version.addon.get_dev_url('versions')),
        'SITE_URL': settings.SITE_URL,
        'email_reason': 'you are an author of this add-on'
    }

    reviewer_context_dict = author_context_dict.copy()
    reviewer_context_dict['url'] = absolutify(
        reverse('reviewers.review',
                kwargs={'addon_id': version.addon.pk,
                        'channel': amo.CHANNEL_CHOICES_API[version.channel]},
                add_prefix=False))
    reviewer_context_dict['email_reason'] = 'you reviewed this add-on'

    staff_cc_context_dict = reviewer_context_dict.copy()
    staff_cc_context_dict['email_reason'] = (
        'you are member of the activity email cc group')

    # Not being localised because we don't know the recipients locale.
    with translation.override('en-US'):
        subject = u'Mozilla Add-ons: %s %s' % (
            version.addon.name, version.version)
    template = template_from_user(note_creator, version)
    from_email = formataddr((note_creator.name, NOTIFICATIONS_FROM_EMAIL))
    send_activity_mail(
        subject, template.render(author_context_dict),
        version, addon_authors, from_email, note.id, perm_setting)

    send_activity_mail(
        subject, template.render(reviewer_context_dict),
        version, reviewers, from_email, note.id, perm_setting)

    send_activity_mail(
        subject, template.render(staff_cc_context_dict),
        version, staff_cc, from_email, note.id, perm_setting)

    if action == amo.LOG.DEVELOPER_REPLY_VERSION:
        version.update(has_info_request=False)

    return note
示例#16
0
def log_and_notify(action,
                   comments,
                   note_creator,
                   version,
                   perm_setting=None,
                   detail_kwargs=None):
    log_kwargs = {
        'user': note_creator,
        'created': datetime.datetime.now(),
    }
    if detail_kwargs is None:
        detail_kwargs = {}
    if comments:
        detail_kwargs['version'] = version.version
        detail_kwargs['comments'] = comments
    else:
        # Just use the name of the action if no comments provided.  Alas we
        # can't know the locale of recipient, and our templates are English
        # only so prevent language jumble by forcing into en-US.
        with no_translation():
            comments = '%s' % action.short
    if detail_kwargs:
        log_kwargs['details'] = detail_kwargs

    note = ActivityLog.create(action, version.addon, version, **log_kwargs)
    if not note:
        return

    # Collect reviewers involved with this version.
    review_perm = (amo.permissions.ADDONS_REVIEW
                   if version.channel == amo.RELEASE_CHANNEL_LISTED else
                   amo.permissions.ADDONS_REVIEW_UNLISTED)
    log_users = {
        alog.user
        for alog in ActivityLog.objects.for_version(version)
        if acl.action_allowed_user(alog.user, review_perm)
    }
    # Collect add-on authors (excl. the person who sent the email.)
    addon_authors = set(version.addon.authors.all()) - {note_creator}
    # Collect staff that want a copy of the email
    staff = set(UserProfile.objects.filter(groups__name=ACTIVITY_MAIL_GROUP))
    # If task_user doesn't exist that's no big issue (i.e. in tests)
    try:
        task_user = {get_task_user()}
    except UserProfile.DoesNotExist:
        task_user = set()
    # Collect reviewers on the thread (excl. the email sender and task user for
    # automated messages).
    reviewers = log_users - addon_authors - task_user - {note_creator}
    staff_cc = staff - reviewers - addon_authors - task_user - {note_creator}
    author_context_dict = {
        'name': version.addon.name,
        'number': version.version,
        'author': note_creator.name,
        'comments': comments,
        'url': absolutify(version.addon.get_dev_url('versions')),
        'SITE_URL': settings.SITE_URL,
        'email_reason': 'you are an author of this add-on'
    }

    reviewer_context_dict = author_context_dict.copy()
    reviewer_context_dict['url'] = absolutify(
        reverse('editors.review',
                kwargs={
                    'addon_id': version.addon.pk,
                    'channel': amo.CHANNEL_CHOICES_API[version.channel]
                },
                add_prefix=False))
    reviewer_context_dict['email_reason'] = 'you reviewed this add-on'

    staff_cc_context_dict = reviewer_context_dict.copy()
    staff_cc_context_dict['email_reason'] = (
        'you are member of the activity email cc group')

    # Not being localised because we don't know the recipients locale.
    with translation.override('en-US'):
        subject = u'Mozilla Add-ons: %s %s' % (version.addon.name,
                                               version.version)
    template = template_from_user(note_creator, version)
    from_email = formataddr((note_creator.name, NOTIFICATIONS_FROM_EMAIL))
    send_activity_mail(subject, template.render(author_context_dict), version,
                       addon_authors, from_email, note.id, perm_setting)

    send_activity_mail(subject, template.render(reviewer_context_dict),
                       version, reviewers, from_email, note.id, perm_setting)

    send_activity_mail(subject, template.render(staff_cc_context_dict),
                       version, staff_cc, from_email, note.id, perm_setting)

    if action == amo.LOG.DEVELOPER_REPLY_VERSION:
        version.update(has_info_request=False)

    return note
示例#17
0
def notify_compatibility_chunk(users, job, data, **kw):
    log.info('[%s@%s] Sending notification mail for job %s.' %
             (len(users), notify_compatibility.rate_limit, job.pk))
    set_user(get_task_user())
    dry_run = data['preview_only']
    app_id = job.target_version.application
    stats = collections.defaultdict(int)
    stats['processed'] = 0
    stats['is_dry_run'] = int(dry_run)

    for user in users:
        stats['processed'] += 1

        try:
            for a in chain(user.passing_addons, user.failing_addons):
                try:
                    results = job.result_set.filter(file__version__addon=a)

                    a.links = [
                        absolutify(
                            reverse('devhub.bulk_compat_result',
                                    args=[a.slug, r.pk])) for r in results
                    ]

                    v = a.current_version or a.latest_version
                    a.compat_link = absolutify(
                        reverse('devhub.versions.edit', args=[a.pk, v.pk]))
                except:
                    task_error = sys.exc_info()
                    log.error(
                        u'Bulk validation email error for user %s, '
                        u'addon %s: %s: %s' %
                        (user.email, a.slug, task_error[0], task_error[1]),
                        exc_info=False)

            context = Context({
                'APPLICATION':
                unicode(amo.APP_IDS[job.application].pretty),
                'VERSION':
                job.target_version.version,
                'PASSING_ADDONS':
                user.passing_addons,
                'FAILING_ADDONS':
                user.failing_addons,
            })

            log.info(
                u'Emailing %s%s for %d addons about '
                'bulk validation job %s' %
                (user.email, ' [PREVIEW]' if dry_run else '',
                 len(user.passing_addons) + len(user.failing_addons), job.pk))
            args = (Template(data['subject']).render(context),
                    Template(data['text']).render(context))
            kwargs = dict(from_email=settings.DEFAULT_FROM_EMAIL,
                          recipient_list=[user.email])
            if dry_run:
                job.preview_notify_mail(*args, **kwargs)
            else:
                stats['author_emailed'] += 1
                send_mail(*args, **kwargs)
                amo.log(amo.LOG.BULK_VALIDATION_USER_EMAILED,
                        user,
                        details={
                            'passing': [a.id for a in user.passing_addons],
                            'failing': [a.id for a in user.failing_addons],
                            'target': job.target_version.version,
                            'application': app_id
                        })
        except:
            task_error = sys.exc_info()
            log.error(u'Bulk validation email error for user %s: %s: %s' %
                      (user.email, task_error[0], task_error[1]),
                      exc_info=False)

    log.info('[%s@%s] bulk email stats for job %s: {%s}' %
             (len(users), notify_compatibility.rate_limit, job.pk, ', '.join(
                 '%s: %s' % (k, stats[k]) for k in sorted(stats.keys()))))
示例#18
0
def notify_about_activity_log(addon,
                              version,
                              note,
                              perm_setting=None,
                              send_to_reviewers=True,
                              send_to_staff=True):
    """Notify relevant users about an ActivityLog note."""
    comments = (note.details or {}).get('comments')
    if not comments:
        # Just use the name of the action if no comments provided.  Alas we
        # can't know the locale of recipient, and our templates are English
        # only so prevent language jumble by forcing into en-US.
        with translation.override(settings.LANGUAGE_CODE):
            comments = '%s' % amo.LOG_BY_ID[note.action].short
    else:
        comments = unescape(comments)

    type_of_sender = type_of_user(note.user, version)
    sender_name = (ADDON_REVIEWER_NAME if type_of_sender
                   == USER_TYPE_ADDON_REVIEWER else note.user.name)

    # Collect add-on authors (excl. the person who sent the email.) and build
    # the context for them.
    addon_authors = set(addon.authors.all()) - {note.user}

    author_context_dict = {
        'name': addon.name,
        'number': version.version,
        'author': sender_name,
        'comments': comments,
        'url': absolutify(addon.get_dev_url('versions')),
        'SITE_URL': settings.SITE_URL,
        'email_reason': 'you are listed as an author of this add-on',
    }

    # Not being localised because we don't know the recipients locale.
    with translation.override('en-US'):
        subject = reviewer_subject = 'Mozilla Add-ons: {} {}'.format(
            addon.name,
            version.version,
        )
    # Build and send the mail for authors.
    template = template_from_user(note.user, version)
    from_email = formataddr((sender_name, NOTIFICATIONS_FROM_EMAIL))
    send_activity_mail(
        subject,
        template.render(author_context_dict),
        version,
        addon_authors,
        from_email,
        note.id,
        perm_setting,
    )

    if send_to_reviewers or send_to_staff:
        # If task_user doesn't exist that's no big issue (i.e. in tests)
        try:
            task_user = {get_task_user()}
        except UserProfile.DoesNotExist:
            task_user = set()
        # Update the author and from_email to use the real name because it will
        # be used in emails to reviewers and staff, and not add-on developers.
        from_email = formataddr((note.user.name, NOTIFICATIONS_FROM_EMAIL))
        reviewer_context_dict = author_context_dict.copy()
        reviewer_context_dict['author'] = note.user.name

    if send_to_reviewers:
        # Collect reviewers on the thread (excl. the email sender and task user
        # for automated messages) and send them their copy.
        log_users = {
            alog.user
            for alog in ActivityLog.objects.for_versions(version)
            if acl.is_user_any_kind_of_reviewer(alog.user)
        }
        reviewers = log_users - addon_authors - task_user - {note.user}
        reviewer_context_dict['url'] = absolutify(
            reverse(
                'reviewers.review',
                kwargs={
                    'addon_id': version.addon.pk,
                    'channel': amo.CHANNEL_CHOICES_API[version.channel],
                },
                add_prefix=False,
            ))
        reviewer_context_dict['email_reason'] = 'you reviewed this add-on'
        send_activity_mail(
            reviewer_subject,
            template.render(reviewer_context_dict),
            version,
            reviewers,
            from_email,
            note.id,
            perm_setting,
        )

    if send_to_staff:
        # Collect staff that want a copy of the email, build the context for
        # them and send them their copy.
        staff = set(
            UserProfile.objects.filter(groups__name=ACTIVITY_MAIL_GROUP))
        staff_cc = staff - reviewers - addon_authors - task_user - {note.user}
        staff_cc_context_dict = reviewer_context_dict.copy()
        staff_cc_context_dict[
            'email_reason'] = 'you are member of the activity email cc group'
        send_activity_mail(
            reviewer_subject,
            template.render(staff_cc_context_dict),
            version,
            staff_cc,
            from_email,
            note.id,
            perm_setting,
        )
示例#19
0
def import_block_from_blocklist(record):
    kinto_id = record.get('id')
    using_db = 'replica' if 'replica' in settings.DATABASES else 'default'
    log.debug('Processing block id: [%s]', kinto_id)
    kinto_import = KintoImport(kinto_id=kinto_id, record=record)

    guid = record.get('guid')
    if not guid:
        kinto_import.outcome = KintoImport.OUTCOME_MISSINGGUID
        kinto_import.save()
        log.error('Kinto %s: GUID is falsey, skipping.', kinto_id)
        return
    version_range = record.get('versionRange', [{}])[0]
    target_application = version_range.get('targetApplication') or [{}]
    target_GUID = target_application[0].get('guid')
    if target_GUID and target_GUID != amo.FIREFOX.guid:
        kinto_import.outcome = KintoImport.OUTCOME_NOTFIREFOX
        kinto_import.save()
        log.error('Kinto %s: targetApplication (%s) is not Firefox, skipping.',
                  kinto_id, target_GUID)
        return
    block_kw = {
        'min_version': version_range.get('minVersion', '0'),
        'max_version': version_range.get('maxVersion', '*'),
        'url': record.get('details', {}).get('bug') or '',
        'reason': record.get('details', {}).get('why') or '',
        'kinto_id': kinto_id,
        'include_in_legacy': True,
        'updated_by': get_task_user(),
    }
    modified_date = datetime.fromtimestamp(
        record.get('last_modified',
                   time.time() * 1000) / 1000)

    if guid.startswith('/'):
        # need to escape the {} brackets or mysql chokes.
        guid_regexp = bracket_open_regex.sub(r'\{', guid[1:-1])
        guid_regexp = bracket_close_regex.sub(r'\}', guid_regexp)
        # we're going to try to split the regex into a list for efficiency.
        guids_list = split_regex_to_list(guid_regexp)
        if guids_list:
            log.debug(
                'Kinto %s: Broke down regex into list; '
                'attempting to create Blocks for guids in %s', kinto_id,
                guids_list)
            addons_guids_qs = Addon.unfiltered.using(using_db).filter(
                guid__in=guids_list).values_list('guid', flat=True)
        else:
            log.debug(
                'Kinto %s: Unable to break down regex into list; '
                'attempting to create Blocks for guids matching [%s]',
                kinto_id, guid_regexp)
            # mysql doesn't support \d - only [:digit:]
            guid_regexp = guid_regexp.replace(r'\d', '[[:digit:]]')
            addons_guids_qs = Addon.unfiltered.using(using_db).filter(
                guid__regex=guid_regexp).values_list('guid', flat=True)
        # We need to mark this id in a way so we know its from a
        # regex guid - otherwise we might accidentally overwrite it.
        block_kw['kinto_id'] = '*' + block_kw['kinto_id']
        regex = True
    else:
        log.debug('Kinto %s: Attempting to create a Block for guid [%s]',
                  kinto_id, guid)
        addons_guids_qs = Addon.unfiltered.using(using_db).filter(
            guid=guid).values_list('guid', flat=True)
        regex = False
    new_blocks = []
    for guid in addons_guids_qs:
        valid_files_qs = File.objects.filter(version__addon__guid=guid,
                                             is_webextension=True)
        if not valid_files_qs.exists():
            log.debug(
                'Kinto %s: Skipped Block for [%s] because it has no '
                'webextension files', kinto_id, guid)
            continue
        (block,
         created) = Block.objects.update_or_create(guid=guid,
                                                   defaults=dict(guid=guid,
                                                                 **block_kw))
        block_activity_log_save(block, change=not created)
        if created:
            log.debug('Kinto %s: Added Block for [%s]', kinto_id, guid)
            block.update(modified=modified_date)
        else:
            log.debug('Kinto %s: Updated Block for [%s]', kinto_id, guid)
        new_blocks.append(block)
    if new_blocks:
        kinto_import.outcome = (KintoImport.OUTCOME_REGEXBLOCKS
                                if regex else KintoImport.OUTCOME_BLOCK)
    else:
        kinto_import.outcome = KintoImport.OUTCOME_NOMATCH
        log.debug('Kinto %s: No addon found', kinto_id)
    kinto_import.save()
示例#20
0
    def from_upload(cls,
                    upload,
                    addon,
                    selected_apps,
                    channel,
                    parsed_data=None):
        """
        Create a Version instance and corresponding File(s) from a
        FileUpload, an Addon, a list of compatible app ids, a channel id and
        the parsed_data generated by parse_addon().

        Note that it's the caller's responsability to ensure the file is valid.
        We can't check for that here because an admin may have overridden the
        validation results.
        """
        from olympia.addons.models import AddonReviewerFlags
        from olympia.addons.utils import RestrictionChecker
        from olympia.git.utils import create_git_extraction_entry

        assert parsed_data is not None

        if addon.status == amo.STATUS_DISABLED:
            raise VersionCreateError(
                'Addon is Mozilla Disabled; no new versions are allowed.')

        if upload.addon and upload.addon != addon:
            raise VersionCreateError(
                'FileUpload was made for a different Addon')

        if not upload.user or not upload.ip_address or not upload.source:
            raise VersionCreateError(
                'FileUpload does not have some required fields')

        if not upload.user.last_login_ip or not upload.user.email:
            raise VersionCreateError(
                'FileUpload user does not have some required fields')

        license_id = None
        if channel == amo.RELEASE_CHANNEL_LISTED:
            previous_version = addon.find_latest_version(channel=channel,
                                                         exclude=())
            if previous_version and previous_version.license_id:
                license_id = previous_version.license_id
        approval_notes = None
        if parsed_data.get('is_mozilla_signed_extension'):
            approval_notes = (
                'This version has been signed with Mozilla internal certificate.'
            )
        version = cls.objects.create(
            addon=addon,
            approval_notes=approval_notes,
            version=parsed_data['version'],
            license_id=license_id,
            channel=channel,
        )
        email = upload.user.email if upload.user and upload.user.email else ''
        with core.override_remote_addr(upload.ip_address):
            # The following log statement is used by foxsec-pipeline.
            # We override the IP because it might be called from a task and we
            # want the original IP from the submitter.
            log.info(
                f'New version: {version!r} ({version.id}) from {upload!r}',
                extra={
                    'email': email,
                    'guid': addon.guid,
                    'upload': upload.uuid.hex,
                    'user_id': upload.user_id,
                    'from_api': upload.source == amo.UPLOAD_SOURCE_API,
                },
            )
            activity.log_create(amo.LOG.ADD_VERSION,
                                version,
                                addon,
                                user=upload.user or get_task_user())

        if addon.type == amo.ADDON_STATICTHEME:
            # We don't let developers select apps for static themes
            selected_apps = [app.id for app in amo.APP_USAGE]

        compatible_apps = {}
        for app in parsed_data.get('apps', []):
            if app.id not in selected_apps:
                # If the user chose to explicitly deselect Firefox for Android
                # we're not creating the respective `ApplicationsVersions`
                # which will have this add-on then be listed only for
                # Firefox specifically.
                continue

            compatible_apps[app.appdata] = ApplicationsVersions(
                version=version, min=app.min, max=app.max, application=app.id)
            compatible_apps[app.appdata].save()

        # Pre-generate _compatible_apps property to avoid accidentally
        # triggering queries with that instance later.
        version._compatible_apps = compatible_apps

        # Create relevant file and update the all_files cached property on the
        # Version, because we might need it afterwards.
        version.all_files = [
            File.from_upload(
                upload=upload,
                version=version,
                parsed_data=parsed_data,
            )
        ]

        version.inherit_nomination(from_statuses=[amo.STATUS_AWAITING_REVIEW])
        version.disable_old_files()

        # After the upload has been copied to its permanent location, delete it
        # from storage. Keep the FileUpload instance (it gets cleaned up by a
        # cron eventually some time after its creation, in amo.cron.gc()),
        # making sure it's associated with the add-on instance.
        storage.delete(upload.path)
        upload.path = ''
        if upload.addon is None:
            upload.addon = addon
        upload.save()

        version_uploaded.send(instance=version, sender=Version)

        if version.is_webextension:
            if (waffle.switch_is_active('enable-yara')
                    or waffle.switch_is_active('enable-customs')
                    or waffle.switch_is_active('enable-wat')):
                ScannerResult.objects.filter(upload_id=upload.id).update(
                    version=version)

        if waffle.switch_is_active('enable-uploads-commit-to-git-storage'):
            # Schedule this version for git extraction.
            transaction.on_commit(
                lambda: create_git_extraction_entry(version=version))

        # Generate a preview and icon for listed static themes
        if (addon.type == amo.ADDON_STATICTHEME
                and channel == amo.RELEASE_CHANNEL_LISTED):
            theme_data = parsed_data.get('theme', {})
            generate_static_theme_preview(theme_data, version.pk)

        # Reset add-on reviewer flags to disable auto-approval and require
        # admin code review if the package has already been signed by mozilla.
        reviewer_flags_defaults = {}
        is_mozilla_signed = parsed_data.get('is_mozilla_signed_extension')
        if upload.validation_timeout:
            reviewer_flags_defaults['needs_admin_code_review'] = True
        if is_mozilla_signed and addon.type != amo.ADDON_LPAPP:
            reviewer_flags_defaults['needs_admin_code_review'] = True
            reviewer_flags_defaults['auto_approval_disabled'] = True

        # Check if the approval should be restricted
        if not RestrictionChecker(upload=upload).is_auto_approval_allowed():
            flag = ('auto_approval_disabled'
                    if channel == amo.RELEASE_CHANNEL_LISTED else
                    'auto_approval_disabled_unlisted')
            reviewer_flags_defaults[flag] = True

        if reviewer_flags_defaults:
            AddonReviewerFlags.objects.update_or_create(
                addon=addon, defaults=reviewer_flags_defaults)

        # Authors need to be notified about auto-approval delay again since
        # they are submitting a new version.
        addon.reset_notified_about_auto_approval_delay()

        # Track the time it took from first upload through validation
        # (and whatever else) until a version was created.
        upload_start = utc_millesecs_from_epoch(upload.created)
        now = datetime.datetime.now()
        now_ts = utc_millesecs_from_epoch(now)
        upload_time = now_ts - upload_start

        log.info('Time for version {version} creation from upload: {delta}; '
                 'created={created}; now={now}'.format(delta=upload_time,
                                                       version=version,
                                                       created=upload.created,
                                                       now=now))
        statsd.timing('devhub.version_created_from_upload', upload_time)

        return version
示例#21
0
def import_block_from_blocklist(record):
    kinto_id = record.get('id')
    using_db = 'replica' if 'replica' in settings.DATABASES else 'default'
    log.debug('Processing block id: [%s]', kinto_id)
    kinto_import = KintoImport(kinto_id=kinto_id, record=record)

    guid = record.get('guid')
    if not guid:
        kinto_import.outcome = KintoImport.OUTCOME_MISSINGGUID
        kinto_import.save()
        log.error('Kinto %s: GUID is falsey, skipping.', kinto_id)
        return
    version_range = record.get('versionRange', [{}])[0]
    target_application = version_range.get('targetApplication') or [{}]
    target_GUID = target_application[0].get('guid')
    if target_GUID and target_GUID != amo.FIREFOX.guid:
        kinto_import.outcome = KintoImport.OUTCOME_NOTFIREFOX
        kinto_import.save()
        log.error(
            'Kinto %s: targetApplication (%s) is not Firefox, skipping.',
            kinto_id, target_GUID)
        return
    block_kw = {
        'min_version': version_range.get('minVersion', '0'),
        'max_version': version_range.get('maxVersion', '*'),
        'url': record.get('details', {}).get('bug'),
        'reason': record.get('details', {}).get('why', ''),
        'kinto_id': kinto_id,
        'include_in_legacy': True,
        'updated_by': get_task_user(),
    }
    modified_date = datetime.fromtimestamp(
        record.get('last_modified', time.time() * 1000) / 1000)

    if guid.startswith('/'):
        # need to escape the {} brackets or mysql chokes.
        guid_regexp = bracket_open_regex.sub(r'\{', guid[1:-1])
        guid_regexp = bracket_close_regex.sub(r'\}', guid_regexp)
        log.debug(
            'Kinto %s: Attempting to create Blocks for addons matching [%s]',
            kinto_id, guid_regexp)
        addons_guids_qs = Addon.unfiltered.using(using_db).filter(
            guid__regex=guid_regexp).values_list('guid', flat=True)
        # We need to mark this id in a way so we know its from a
        # regex guid - otherwise we might accidentally overwrite it.
        block_kw['kinto_id'] = '*' + block_kw['kinto_id']
        regex = True
    else:
        log.debug(
            'Kinto %s: Attempting to create a Block for guid [%s]',
            kinto_id, guid)
        addons_guids_qs = Addon.unfiltered.using(using_db).filter(
            guid=guid).values_list('guid', flat=True)
        regex = False
    for guid in addons_guids_qs:
        (block, created) = Block.objects.update_or_create(
            guid=guid, defaults=dict(guid=guid, **block_kw))
        block_activity_log_save(block, change=not created)
        if created:
            log.debug('Kinto %s: Added Block for [%s]', kinto_id, block.guid)
            block.update(modified=modified_date)
        else:
            log.debug('Kinto %s: Updated Block for [%s]', kinto_id, block.guid)
    if addons_guids_qs:
        kinto_import.outcome = (
            KintoImport.OUTCOME_REGEXBLOCKS if regex else
            KintoImport.OUTCOME_BLOCK
        )
    else:
        kinto_import.outcome = KintoImport.OUTCOME_NOMATCH
        log.debug('Kinto %s: No addon found', kinto_id)
    kinto_import.save()
示例#22
0
def notify_compatibility_chunk(users, job, data, **kw):
    log.info('[%s@%s] Sending notification mail for job %s.'
             % (len(users), notify_compatibility.rate_limit, job.pk))
    set_user(get_task_user())
    dry_run = data['preview_only']
    app_id = job.target_version.application
    stats = collections.defaultdict(int)
    stats['processed'] = 0
    stats['is_dry_run'] = int(dry_run)

    for user in users:
        stats['processed'] += 1

        try:
            for a in chain(user.passing_addons, user.failing_addons):
                try:
                    results = job.result_set.filter(file__version__addon=a)

                    a.links = [absolutify(reverse('devhub.bulk_compat_result',
                                                  args=[a.slug, r.pk]))
                               for r in results]

                    v = a.current_version or a.latest_version
                    a.compat_link = absolutify(reverse('devhub.versions.edit',
                                                       args=[a.pk, v.pk]))
                except:
                    task_error = sys.exc_info()
                    log.error(u'Bulk validation email error for user %s, '
                              u'addon %s: %s: %s'
                              % (user.email, a.slug,
                                 task_error[0], task_error[1]), exc_info=False)

            context = Context({
                'APPLICATION': unicode(amo.APP_IDS[job.application].pretty),
                'VERSION': job.target_version.version,
                'PASSING_ADDONS': user.passing_addons,
                'FAILING_ADDONS': user.failing_addons,
            })

            log.info(u'Emailing %s%s for %d addons about '
                     'bulk validation job %s'
                     % (user.email,
                        ' [PREVIEW]' if dry_run else '',
                        len(user.passing_addons) + len(user.failing_addons),
                        job.pk))
            args = (Template(data['subject']).render(context),
                    Template(data['text']).render(context))
            kwargs = dict(from_email=settings.DEFAULT_FROM_EMAIL,
                          recipient_list=[user.email])
            if dry_run:
                job.preview_notify_mail(*args, **kwargs)
            else:
                stats['author_emailed'] += 1
                send_mail(*args, **kwargs)
                amo.log(
                    amo.LOG.BULK_VALIDATION_USER_EMAILED,
                    user,
                    details={'passing': [a.id for a in user.passing_addons],
                             'failing': [a.id for a in user.failing_addons],
                             'target': job.target_version.version,
                             'application': app_id})
        except:
            task_error = sys.exc_info()
            log.error(u'Bulk validation email error for user %s: %s: %s'
                      % (user.email,
                         task_error[0], task_error[1]), exc_info=False)

    log.info('[%s@%s] bulk email stats for job %s: {%s}'
             % (len(users), notify_compatibility.rate_limit, job.pk,
                ', '.join('%s: %s' % (k, stats[k])
                          for k in sorted(stats.keys()))))
示例#23
0
    def create_version(self, addon=None):
        from olympia.addons.models import Addon
        from olympia.files.models import FileUpload
        from olympia.files.utils import parse_addon
        from olympia.versions.models import Version
        from olympia.versions.utils import get_next_version_number

        version_number = '1.0'

        # If passing an existing add-on, we need to bump the version number
        # to avoid clashes, and also perform a few checks.
        if addon is not None:
            # Obviously we want an add-on with the right type.
            if addon.type != amo.ADDON_SITE_PERMISSION:
                raise ImproperlyConfigured(
                    'SitePermissionVersionCreator was instantiated with non '
                    'site-permission add-on'
                )
            # If the user isn't an author, something is wrong.
            if not addon.authors.filter(pk=self.user.pk).exists():
                raise ImproperlyConfigured(
                    'SitePermissionVersionCreator was instantiated with a '
                    'bogus addon/user'
                )
            # Changing the origins isn't supported at the moment.
            latest_version = addon.find_latest_version(
                exclude=(), channel=amo.RELEASE_CHANNEL_UNLISTED
            )
            previous_origins = sorted(
                latest_version.installorigin_set.all().values_list('origin', flat=True)
            )
            if previous_origins != self.install_origins:
                raise ImproperlyConfigured(
                    'SitePermissionVersionCreator was instantiated with an '
                    'addon that has different origins'
                )

            version_number = get_next_version_number(addon)

        # Create the manifest, with more user-friendly name & description built
        # from install_origins/site_permissions, and then the zipfile with that
        # manifest inside.
        manifest_data = self._create_manifest(version_number)
        file_obj = self._create_zipfile(manifest_data)

        # Parse the zip we just created. The user needs to be the Mozilla User
        # because regular submissions of this type of add-on is forbidden to
        # normal users.
        parsed_data = parse_addon(
            file_obj,
            addon=addon,
            user=get_task_user(),
        )

        with core.override_remote_addr(self.remote_addr):
            if addon is None:
                # Create the Addon instance (without a Version/File at this point).
                addon = Addon.initialize_addon_from_upload(
                    data=parsed_data,
                    upload=file_obj,
                    channel=amo.RELEASE_CHANNEL_UNLISTED,
                    user=self.user,
                )

            # Create the FileUpload that will become the File+Version.
            upload = FileUpload.from_post(
                file_obj,
                filename=file_obj.name,
                size=file_obj.size,
                addon=addon,
                version=version_number,
                channel=amo.RELEASE_CHANNEL_UNLISTED,
                user=self.user,
                source=amo.UPLOAD_SOURCE_GENERATED,
            )

        # And finally create the Version instance from the FileUpload.
        return Version.from_upload(
            upload,
            addon,
            amo.RELEASE_CHANNEL_UNLISTED,
            selected_apps=[x[0] for x in amo.APPS_CHOICES],
            parsed_data=parsed_data,
        )
示例#24
0
def import_block_from_blocklist(record):
    legacy_id = record.get('id')
    using_db = get_replica()
    log.info('Processing block id: [%s]', legacy_id)
    legacy_import, import_created = LegacyImport.objects.update_or_create(
        legacy_id=legacy_id,
        defaults={
            'record': record,
            'timestamp': record.get('last_modified')
        },
    )
    if not import_created:
        log.info('LegacyRS %s: updating existing LegacyImport object',
                 legacy_id)
        existing_block_ids = list(
            Block.objects.filter(legacy_id__in=(legacy_id,
                                                f'*{legacy_id}')).values_list(
                                                    'id', flat=True))

    guid = record.get('guid')
    if not guid:
        legacy_import.outcome = LegacyImport.OUTCOME_MISSINGGUID
        legacy_import.save()
        log.error('LegacyRS %s: GUID is falsey, skipping.', legacy_id)
        return
    version_range = (record.get('versionRange') or [{}])[0]
    target_application = version_range.get('targetApplication') or [{}]
    target_GUID = target_application[0].get('guid')
    if target_GUID and target_GUID != amo.FIREFOX.guid:
        legacy_import.outcome = LegacyImport.OUTCOME_NOTFIREFOX
        legacy_import.save()
        log.error(
            'LegacyRS %s: targetApplication (%s) is not Firefox, skipping.',
            legacy_id,
            target_GUID,
        )
        return
    block_kw = {
        'min_version': version_range.get('minVersion', '0'),
        'max_version': version_range.get('maxVersion', '*'),
        'url': record.get('details', {}).get('bug') or '',
        'reason': record.get('details', {}).get('why') or '',
        'legacy_id': legacy_id,
        'updated_by': get_task_user(),
    }
    modified_date = datetime.fromtimestamp(
        record.get('last_modified', datetime_to_ts()) / 1000)

    if guid.startswith('/'):
        # need to escape the {} brackets or mysql chokes.
        guid_regexp = bracket_open_regex.sub(r'\{', guid[1:-1])
        guid_regexp = bracket_close_regex.sub(r'\}', guid_regexp)
        # we're going to try to split the regex into a list for efficiency.
        guids_list = split_regex_to_list(guid_regexp)
        if guids_list:
            log.info(
                'LegacyRS %s: Broke down regex into list; '
                'attempting to create Blocks for guids in %s',
                legacy_id,
                guids_list,
            )
            statsd.incr('blocklist.tasks.import_blocklist.record_guid',
                        count=len(guids_list))
            addons_guids_qs = (Addon.unfiltered.using(using_db).filter(
                guid__in=guids_list).values_list('guid', flat=True))
        else:
            log.info(
                'LegacyRS %s: Unable to break down regex into list; '
                'attempting to create Blocks for guids matching [%s]',
                legacy_id,
                guid_regexp,
            )
            # mysql doesn't support \d - only [:digit:]
            guid_regexp = guid_regexp.replace(r'\d', '[[:digit:]]')
            addons_guids_qs = (Addon.unfiltered.using(using_db).filter(
                guid__regex=guid_regexp).values_list('guid', flat=True))
        # We need to mark this id in a way so we know its from a
        # regex guid - otherwise we might accidentally overwrite it.
        block_kw['legacy_id'] = '*' + block_kw['legacy_id']
        regex = True
    else:
        log.info('LegacyRS %s: Attempting to create a Block for guid [%s]',
                 legacy_id, guid)
        statsd.incr('blocklist.tasks.import_blocklist.record_guid')
        addons_guids_qs = (Addon.unfiltered.using(using_db).filter(
            guid=guid).values_list('guid', flat=True))
        regex = False
    new_blocks = []
    for guid in addons_guids_qs:
        valid_files_qs = File.objects.filter(version__addon__guid=guid,
                                             is_webextension=True)
        if not valid_files_qs.exists():
            log.info(
                'LegacyRS %s: Skipped Block for [%s] because it has no '
                'webextension files',
                legacy_id,
                guid,
            )
            statsd.incr('blocklist.tasks.import_blocklist.block_skipped')
            continue
        (block,
         created) = Block.objects.update_or_create(guid=guid,
                                                   defaults=dict(guid=guid,
                                                                 **block_kw))
        block_activity_log_save(block, change=not created)
        if created:
            log.info('LegacyRS %s: Added Block for [%s]', legacy_id, guid)
            statsd.incr('blocklist.tasks.import_blocklist.block_added')
            block.update(modified=modified_date)
        else:
            log.info('LegacyRS %s: Updated Block for [%s]', legacy_id, guid)
            statsd.incr('blocklist.tasks.import_blocklist.block_updated')
        new_blocks.append(block)
    if new_blocks:
        legacy_import.outcome = (LegacyImport.OUTCOME_REGEXBLOCKS
                                 if regex else LegacyImport.OUTCOME_BLOCK)
    else:
        legacy_import.outcome = LegacyImport.OUTCOME_NOMATCH
        log.info('LegacyRS %s: No addon found', legacy_id)
    if not import_created:
        # now reconcile the blocks that were connected to the import last time
        # but weren't changed this time - i.e. blocks we need to delete
        delete_qs = Block.objects.filter(id__in=existing_block_ids).exclude(
            id__in=(block.id for block in new_blocks))
        for block in delete_qs:
            block_activity_log_delete(block,
                                      delete_user=block_kw['updated_by'])
            block.delete()
            statsd.incr('blocklist.tasks.import_blocklist.block_deleted')

    legacy_import.save()

    if import_created:
        statsd.incr('blocklist.tasks.import_blocklist.new_record_processed')
    else:
        statsd.incr(
            'blocklist.tasks.import_blocklist.modified_record_processed')
示例#25
0
def sign_addons(addon_ids, force=False, send_emails=True, **kw):
    """Used to sign all the versions of an addon.

    This is used in the 'process_addons --task resign_addons_for_cose'
    management command.

    This is also used to resign some promoted addons after they've been added
    to a group (or paid).

    It also bumps the version number of the file and the Version, so the
    Firefox extension update mechanism picks this new signed version and
    installs it.
    """
    log.info(f'[{len(addon_ids)}] Signing addons.')

    mail_subject, mail_message = MAIL_COSE_SUBJECT, MAIL_COSE_MESSAGE

    # query everything except for search-plugins as they're generally
    # not signed
    current_versions = Addon.objects.filter(id__in=addon_ids).values_list(
        '_current_version', flat=True
    )
    qset = Version.objects.filter(id__in=current_versions)

    addons_emailed = set()
    task_user = get_task_user()

    for version in qset:
        file_obj = version.file
        # We only sign files that have been reviewed
        if file_obj.status not in amo.REVIEWED_STATUSES:
            log.info(
                'Not signing addon {}, version {} (no files)'.format(
                    version.addon, version
                )
            )
            continue

        log.info(f'Signing addon {version.addon}, version {version}')
        bumped_version_number = get_new_version_number(version.version)
        did_sign = False  # Did we sign at the file?

        if not os.path.isfile(file_obj.file_path):
            log.info(f'File {file_obj.pk} does not exist, skip')
            continue

        # Save the original file, before bumping the version.
        backup_path = f'{file_obj.file_path}.backup_signature'
        shutil.copy(file_obj.file_path, backup_path)

        try:
            # Need to bump the version (modify manifest file)
            # before the file is signed.
            update_version_number(file_obj, bumped_version_number)
            did_sign = bool(sign_file(file_obj))
            if not did_sign:  # We didn't sign, so revert the version bump.
                shutil.move(backup_path, file_obj.file_path)
        except Exception:
            log.error(f'Failed signing file {file_obj.pk}', exc_info=True)
            # Revert the version bump, restore the backup.
            shutil.move(backup_path, file_obj.file_path)

        # Now update the Version model, if we signed at least one file.
        if did_sign:
            previous_version_str = str(version.version)
            version.update(version=bumped_version_number)
            addon = version.addon
            ActivityLog.create(
                amo.LOG.VERSION_RESIGNED,
                addon,
                version,
                previous_version_str,
                user=task_user,
            )
            if send_emails and addon.pk not in addons_emailed:
                # Send a mail to the owners/devs warning them we've
                # automatically signed their addon.
                qs = AddonUser.objects.filter(
                    role=amo.AUTHOR_ROLE_OWNER, addon=addon
                ).exclude(user__email__isnull=True)
                emails = qs.values_list('user__email', flat=True)
                subject = mail_subject
                message = mail_message.format(addon=addon.name)
                amo.utils.send_mail(
                    subject,
                    message,
                    recipient_list=emails,
                    headers={'Reply-To': '*****@*****.**'},
                )
                addons_emailed.add(addon.pk)
示例#26
0
def notify_about_activity_log(addon, version, note, perm_setting=None,
                              send_to_reviewers=True, send_to_staff=True):
    """Notify relevant users about an ActivityLog note."""
    comments = (note.details or {}).get('comments')
    if not comments:
        # Just use the name of the action if no comments provided.  Alas we
        # can't know the locale of recipient, and our templates are English
        # only so prevent language jumble by forcing into en-US.
        with translation.override(settings.LANGUAGE_CODE):
            comments = '%s' % amo.LOG_BY_ID[note.action].short
    else:
        htmlparser = HTMLParser()
        comments = htmlparser.unescape(comments)

    # Collect add-on authors (excl. the person who sent the email.) and build
    # the context for them.
    addon_authors = set(addon.authors.all()) - {note.user}

    author_context_dict = {
        'name': addon.name,
        'number': version.version,
        'author': note.user.name,
        'comments': comments,
        'url': absolutify(addon.get_dev_url('versions')),
        'SITE_URL': settings.SITE_URL,
        'email_reason': 'you are listed as an author of this add-on',
        'is_info_request': note.action == amo.LOG.REQUEST_INFORMATION.id,
    }

    # Not being localised because we don't know the recipients locale.
    with translation.override('en-US'):
        if note.action == amo.LOG.REQUEST_INFORMATION.id:
            if addon.pending_info_request:
                days_left = (
                    # We pad the time left with an extra hour so that the email
                    # does not end up saying "6 days left" because a few
                    # seconds or minutes passed between the datetime was saved
                    # and the email was sent.
                    addon.pending_info_request + timedelta(hours=1) -
                    datetime.now()
                ).days
                if days_left > 9:
                    author_context_dict['number_of_days_left'] = (
                        '%d days' % days_left)
                elif days_left > 1:
                    author_context_dict['number_of_days_left'] = (
                        '%s (%d) days' % (apnumber(days_left), days_left))
                else:
                    author_context_dict['number_of_days_left'] = 'one (1) day'
            subject = u'Mozilla Add-ons: Action Required for %s %s' % (
                addon.name, version.version)
            reviewer_subject = u'Mozilla Add-ons: %s %s' % (
                addon.name, version.version)
        else:
            subject = reviewer_subject = u'Mozilla Add-ons: %s %s' % (
                addon.name, version.version)
    # Build and send the mail for authors.
    template = template_from_user(note.user, version)
    from_email = formataddr((note.user.name, NOTIFICATIONS_FROM_EMAIL))
    send_activity_mail(
        subject, template.render(author_context_dict),
        version, addon_authors, from_email, note.id, perm_setting)

    if send_to_reviewers or send_to_staff:
        # If task_user doesn't exist that's no big issue (i.e. in tests)
        try:
            task_user = {get_task_user()}
        except UserProfile.DoesNotExist:
            task_user = set()

    if send_to_reviewers:
        # Collect reviewers on the thread (excl. the email sender and task user
        # for automated messages), build the context for them and send them
        # their copy.
        log_users = {
            alog.user for alog in ActivityLog.objects.for_version(version) if
            acl.is_user_any_kind_of_reviewer(alog.user)}
        reviewers = log_users - addon_authors - task_user - {note.user}
        reviewer_context_dict = author_context_dict.copy()
        reviewer_context_dict['url'] = absolutify(
            reverse('reviewers.review',
                    kwargs={
                        'addon_id': version.addon.pk,
                        'channel': amo.CHANNEL_CHOICES_API[version.channel]
                    }, add_prefix=False))
        reviewer_context_dict['email_reason'] = 'you reviewed this add-on'
        send_activity_mail(
            reviewer_subject, template.render(reviewer_context_dict),
            version, reviewers, from_email, note.id, perm_setting)

    if send_to_staff:
        # Collect staff that want a copy of the email, build the context for
        # them and send them their copy.
        staff = set(
            UserProfile.objects.filter(groups__name=ACTIVITY_MAIL_GROUP))
        staff_cc = (
            staff - reviewers - addon_authors - task_user - {note.user})
        staff_cc_context_dict = reviewer_context_dict.copy()
        staff_cc_context_dict['email_reason'] = (
            'you are member of the activity email cc group')
        send_activity_mail(
            reviewer_subject, template.render(staff_cc_context_dict),
            version, staff_cc, from_email, note.id, perm_setting)
示例#27
0
def log_and_notify(action, comments, note_creator, version, perm_setting=None,
                   detail_kwargs=None):
    log_kwargs = {
        'user': note_creator,
        'created': datetime.datetime.now(),
    }
    if detail_kwargs is None:
        detail_kwargs = {}
    if comments:
        detail_kwargs['version'] = version.version
        detail_kwargs['comments'] = comments
    else:
        # Just use the name of the action if no comments provided.  Alas we
        # can't know the locale of recipient, and our templates are English
        # only so prevent language jumble by forcing into en-US.
        with no_translation():
            comments = '%s' % action.short
    if detail_kwargs:
        log_kwargs['details'] = detail_kwargs

    note = ActivityLog.create(action, version.addon, version, **log_kwargs)

    # Collect reviewers involved with this version.
    review_perm = (amo.permissions.ADDONS_REVIEW
                   if version.channel == amo.RELEASE_CHANNEL_LISTED
                   else amo.permissions.ADDONS_REVIEW_UNLISTED)
    log_users = {
        alog.user for alog in ActivityLog.objects.for_version(version) if
        acl.action_allowed_user(alog.user, review_perm)}
    # Collect add-on authors (excl. the person who sent the email.)
    addon_authors = set(version.addon.authors.all()) - {note_creator}
    # Collect staff that want a copy of the email
    staff_cc = set(
        UserProfile.objects.filter(groups__name=ACTIVITY_MAIL_GROUP))
    # If task_user doesn't exist that's no big issue (i.e. in tests)
    try:
        task_user = {get_task_user()}
    except UserProfile.DoesNotExist:
        task_user = set()
    # Collect reviewers on the thread (excl. the email sender and task user for
    # automated messages).
    reviewers = ((log_users | staff_cc) - addon_authors - task_user -
                 {note_creator})
    author_context_dict = {
        'name': version.addon.name,
        'number': version.version,
        'author': note_creator.name,
        'comments': comments,
        'url': absolutify(version.addon.get_dev_url('versions')),
        'SITE_URL': settings.SITE_URL,
    }
    reviewer_context_dict = author_context_dict.copy()
    reviewer_context_dict['url'] = absolutify(
        reverse('editors.review', args=[version.addon.pk], add_prefix=False))

    # Not being localised because we don't know the recipients locale.
    with translation.override('en-US'):
        subject = u'Mozilla Add-ons: %s %s %s' % (
            version.addon.name, version.version, action.short)
    template = template_from_user(note_creator, version)
    send_activity_mail(
        subject, template.render(Context(author_context_dict)), version,
        addon_authors, settings.EDITORS_EMAIL, perm_setting)
    send_activity_mail(
        subject, template.render(Context(reviewer_context_dict)), version,
        reviewers, settings.EDITORS_EMAIL, perm_setting)
    return note