class AbuseReport(ModelBase): # Note: those choices don't need to be translated for now, the # human-readable values are only exposed in the admin. ADDON_SIGNATURES = APIChoicesWithNone( ('CURATED_AND_PARTNER', 1, 'Curated and partner'), ('CURATED', 2, 'Curated'), ('PARTNER', 3, 'Partner'), ('NON_CURATED', 4, 'Non-curated'), ('UNSIGNED', 5, 'Unsigned'), ('BROKEN', 6, 'Broken'), ('UNKNOWN', 7, 'Unknown'), ('MISSING', 8, 'Missing'), ('PRELIMINARY', 9, 'Preliminary'), ('SIGNED', 10, 'Signed'), ('SYSTEM', 11, 'System'), ('PRIVILEGED', 12, 'Privileged'), ) REASONS = APIChoicesWithNone( ('DAMAGE', 1, 'Damages computer and/or data'), ('SPAM', 2, 'Creates spam or advertising'), ('SETTINGS', 3, 'Changes search / homepage / new tab page ' 'without informing user'), # `4` was previously 'New tab takeover' but has been merged into the # previous one. We avoid re-using the value. ('BROKEN', 5, "Doesn’t work, breaks websites, or slows Firefox down"), ('POLICY', 6, 'Hateful, violent, or illegal content'), ('DECEPTIVE', 7, "Pretends to be something it’s not"), # `8` was previously "Doesn't work" but has been merged into the # previous one. We avoid re-using the value. ('UNWANTED', 9, "Wasn't wanted / impossible to get rid of"), # `10` was previously "Other". We avoid re-using the value. ('OTHER', 127, 'Other'), ) # https://searchfox.org # /mozilla-central/source/toolkit/components/telemetry/Events.yaml#122-131 # Firefox submits values in lowercase, with '-' and ':' changed to '_'. ADDON_INSTALL_METHODS = APIChoicesWithNone( ('AMWEBAPI', 1, 'Add-on Manager Web API'), ('LINK', 2, 'Direct link'), ('INSTALLTRIGGER', 3, 'Install Trigger'), ('INSTALL_FROM_FILE', 4, 'From File'), ('MANAGEMENT_WEBEXT_API', 5, 'Webext management API'), ('DRAG_AND_DROP', 6, 'Drag & Drop'), ('SIDELOAD', 7, 'Sideload'), # Values between 8 and 13 are obsolete, we use to merge # install source and method into addon_install_method before deciding # to split the two like Firefox does, so these 6 values are only kept # for backwards-compatibility with older reports and older versions of # Firefox that still only submit that. ('FILE_URL', 8, 'File URL'), ('ENTERPRISE_POLICY', 9, 'Enterprise Policy'), ('DISTRIBUTION', 10, 'Included in build'), ('SYSTEM_ADDON', 11, 'System Add-on'), ('TEMPORARY_ADDON', 12, 'Temporary Add-on'), ('SYNC', 13, 'Sync'), # Back to normal values. ('URL', 14, 'URL'), # Our own catch-all. The serializer expects it to be called "OTHER". ('OTHER', 127, 'Other'), ) ADDON_INSTALL_SOURCES = APIChoicesWithNone( ('ABOUT_ADDONS', 1, 'Add-ons Manager'), ('ABOUT_DEBUGGING', 2, 'Add-ons Debugging'), ('ABOUT_PREFERENCES', 3, 'Preferences'), ('AMO', 4, 'AMO'), ('APP_PROFILE', 5, 'App Profile'), ('DISCO', 6, 'Disco Pane'), ('DISTRIBUTION', 7, 'Included in build'), ('EXTENSION', 8, 'Extension'), ('ENTERPRISE_POLICY', 9, 'Enterprise Policy'), ('FILE_URL', 10, 'File URL'), ('GMP_PLUGIN', 11, 'GMP Plugin'), ('INTERNAL', 12, 'Internal'), ('PLUGIN', 13, 'Plugin'), ('RTAMO', 14, 'Return to AMO'), ('SYNC', 15, 'Sync'), ('SYSTEM_ADDON', 16, 'System Add-on'), ('TEMPORARY_ADDON', 17, 'Temporary Add-on'), ('UNKNOWN', 18, 'Unknown'), # Our own catch-all. The serializer expects it to be called "OTHER". ('OTHER', 127, 'Other'), ) REPORT_ENTRY_POINTS = APIChoicesWithNone( ('UNINSTALL', 1, 'Uninstall'), ('MENU', 2, 'Menu'), ('TOOLBAR_CONTEXT_MENU', 3, 'Toolbar context menu'), ('AMO', 4, 'AMO'), ) STATES = Choices( ('UNTRIAGED', 1, 'Untriaged'), ('VALID', 2, 'Valid'), ('SUSPICIOUS', 3, 'Suspicious'), ('DELETED', 4, 'Deleted'), ) # NULL if the reporter is anonymous. reporter = models.ForeignKey(UserProfile, null=True, blank=True, related_name='abuse_reported', on_delete=models.SET_NULL) country_code = models.CharField(max_length=2, default=None, null=True) # An abuse report can be for an addon or a user. # If user is non-null then both addon and guid should be null. # If user is null then addon should be non-null if guid was in our DB, # otherwise addon will be null also. # If both addon and user is null guid should be set. addon = models.ForeignKey(Addon, null=True, related_name='abuse_reports', on_delete=models.CASCADE) guid = models.CharField(max_length=255, null=True) user = models.ForeignKey(UserProfile, null=True, related_name='abuse_reports', on_delete=models.SET_NULL) message = models.TextField(blank=True) state = models.PositiveSmallIntegerField(default=STATES.UNTRIAGED, choices=STATES.choices) # Extra optional fields for more information, giving some context that is # meant to be extracted automatically by the client (i.e. Firefox) and # submitted via the API. client_id = models.CharField(default=None, max_length=64, blank=True, null=True) addon_name = models.CharField(default=None, max_length=255, blank=True, null=True) addon_summary = models.CharField(default=None, max_length=255, blank=True, null=True) addon_version = models.CharField(default=None, max_length=255, blank=True, null=True) addon_signature = models.PositiveSmallIntegerField( default=None, choices=ADDON_SIGNATURES.choices, blank=True, null=True) application = models.PositiveSmallIntegerField(default=amo.FIREFOX.id, choices=amo.APPS_CHOICES, blank=True, null=True) application_version = models.CharField(default=None, max_length=255, blank=True, null=True) application_locale = models.CharField(default=None, max_length=255, blank=True, null=True) operating_system = models.CharField(default=None, max_length=255, blank=True, null=True) operating_system_version = models.CharField(default=None, max_length=255, blank=True, null=True) install_date = models.DateTimeField(default=None, blank=True, null=True) reason = models.PositiveSmallIntegerField(default=None, choices=REASONS.choices, blank=True, null=True) addon_install_origin = models.CharField(default=None, max_length=255, blank=True, null=True) addon_install_method = models.PositiveSmallIntegerField( default=None, choices=ADDON_INSTALL_METHODS.choices, blank=True, null=True) addon_install_source = models.PositiveSmallIntegerField( default=None, choices=ADDON_INSTALL_SOURCES.choices, blank=True, null=True) report_entry_point = models.PositiveSmallIntegerField( default=None, choices=REPORT_ENTRY_POINTS.choices, blank=True, null=True) unfiltered = AbuseReportManager(include_deleted=True) objects = AbuseReportManager() class Meta: db_table = 'abuse_reports' # See comment in addons/models.py about base_manager_name. It needs to # be unfiltered to prevent exceptions when dealing with relations or # saving already deleted objects. base_manager_name = 'unfiltered' indexes = [ models.Index(fields=('created', ), name='created_idx'), ] @property def metadata(self): """ Dict of metadata about this report. Only includes non-null values. """ data = {} field_names = ('client_id', 'addon_name', 'addon_summary', 'addon_version', 'addon_signature', 'application', 'application_version', 'application_locale', 'operating_system', 'operating_system_version', 'install_date', 'reason', 'addon_install_origin', 'addon_install_method', 'report_entry_point') for field_name in field_names: value = self.__dict__[field_name] # Only include values that matter. if value is not None: field = self._meta.get_field(field_name) # If it's a choice field, display the "pretty" version. if field.choices: value = getattr(self, 'get_%s_display' % field_name)() data[field_name] = value return data def delete(self, *args, **kwargs): # AbuseReports are soft-deleted. Note that we keep relations, because # the only possible relations are to users and add-ons, which are also # soft-deleted. return self.update(state=self.STATES.DELETED) @classmethod def lookup_country_code_from_ip(cls, ip): try: # Early check to avoid initializing GeoIP2 on invalid addresses if not ip: raise forms.ValidationError('No IP') validate_ipv46_address(ip) geoip = GeoIP2() value = geoip.country_code(ip) # Annoyingly, we have to catch both django's GeoIP2Exception (setup # issue) and geoip2's GeoIP2Error (lookup issue) except (forms.ValidationError, GeoIP2Exception, GeoIP2Error): value = '' return value @property def target(self): return self.addon or self.user @property def type(self): with translation.override(settings.LANGUAGE_CODE): if self.addon and self.addon.type in amo.ADDON_TYPE: type_ = (translation.ugettext(amo.ADDON_TYPE[self.addon.type])) elif self.user: type_ = 'User' else: type_ = 'Addon' return type_ def __str__(self): name = self.target.name if self.target else self.guid return u'[%s] Abuse Report for %s' % (self.type, name)
class AbuseReport(ModelBase): # Note: those choices don't need to be translated for now, the # human-readable values are only exposed in the admin. The values will be # updated once they are finalized in the PRD. ADDON_SIGNATURES = APIChoicesWithNone() REASONS = APIChoicesWithNone( ('MALWARE', 1, 'Malware'), ('SPAM_OR_ADVERTISING', 2, 'Spam / Advertising'), ('SEARCH_TAKEOVER', 3, 'Search takeover'), ('NEW_TAB_TAKEOVER', 4, 'New tab takeover'), ('BREAKS_WEBSITES', 5, 'Breaks websites'), ('OFFENSIVE', 6, 'Offensive'), ('DOES_NOT_MATCH_DESCRIPTION', 7, 'Doesn\'t match description'), ('DOES_NOT_WORK', 8, 'Doesn\'t work'), ) ADDON_INSTALL_METHODS = APIChoicesWithNone( ('AMWEBAPI', 1, 'Add-on Manager Web API'), ('LINK', 2, 'Direct link'), ('INSTALLTRIGGER', 3, 'Install Trigger'), ('INSTALL-FROM-FILE', 4, 'From File'), ('MANAGEMENT-WEBEXT-API', 5, 'Webext management API'), ('DRAG-AND-DROP', 6, 'Drag & Drop'), ('SIDELOAD', 7, 'Sideload'), ) REPORT_ENTRY_POINTS = APIChoicesWithNone( ('UNINSTALL', 1, 'Uninstall'), ('MENU', 2, 'Menu'), ) # NULL if the reporter is anonymous. reporter = models.ForeignKey(UserProfile, null=True, blank=True, related_name='abuse_reported', on_delete=models.SET_NULL) # ip_address should be removed in a future release once we've migrated # existing reports in the database to 'country'. ip_address = models.CharField(max_length=255, default=None, null=True) country_code = models.CharField(max_length=2, default=None, null=True) # An abuse report can be for an addon or a user. # If user is non-null then both addon and guid should be null. # If user is null then addon should be non-null if guid was in our DB, # otherwise addon will be null also. # If both addon and user is null guid should be set. addon = models.ForeignKey(Addon, null=True, related_name='abuse_reports', on_delete=models.CASCADE) guid = models.CharField(max_length=255, null=True) user = models.ForeignKey(UserProfile, null=True, related_name='abuse_reports', on_delete=models.SET_NULL) message = models.TextField() # Extra optional fields for more information, giving some context that is # meant to be extracted automatically by the client (i.e. Firefox) and # submitted via the API. client_id = models.CharField(default=None, max_length=64, blank=True, null=True) addon_name = models.CharField(default=None, max_length=255, blank=True, null=True) addon_summary = models.CharField(default=None, max_length=255, blank=True, null=True) addon_version = models.CharField(default=None, max_length=255, blank=True, null=True) addon_signature = models.PositiveSmallIntegerField( default=None, choices=ADDON_SIGNATURES.choices, blank=True, null=True) application = models.PositiveSmallIntegerField(default=amo.FIREFOX.id, choices=amo.APPS_CHOICES, blank=True, null=True) application_version = models.CharField(default=None, max_length=255, blank=True, null=True) application_locale = models.CharField(default=None, max_length=255, blank=True, null=True) operating_system = models.CharField(default=None, max_length=255, blank=True, null=True) operating_system_version = models.CharField(default=None, max_length=255, blank=True, null=True) install_date = models.DateTimeField(default=None, blank=True, null=True) reason = models.PositiveSmallIntegerField(default=None, choices=REASONS.choices, blank=True, null=True) addon_install_origin = models.CharField(default=None, max_length=255, blank=True, null=True) addon_install_method = models.PositiveSmallIntegerField( default=None, choices=ADDON_INSTALL_METHODS.choices, blank=True, null=True) report_entry_point = models.PositiveSmallIntegerField( default=None, choices=REPORT_ENTRY_POINTS.choices, blank=True, null=True) class Meta: db_table = 'abuse_reports' def send(self): if self.reporter: user_name = '%s (%s)' % (self.reporter.name, self.reporter.email) else: user_name = 'An anonymous user' target_url = ('%s%s' % (settings.SITE_URL, self.target.get_url_path()) if self.target else 'GUID not in database') name = self.target.name if self.target else self.guid msg = u'%s reported abuse for %s (%s).\n\n%s' % ( user_name, name, target_url, self.message) send_mail(six.text_type(self), msg, recipient_list=(settings.ABUSE_EMAIL, )) def save(self, *args, **kwargs): creation = not self.pk super(AbuseReport, self).save(*args, **kwargs) if creation: self.send() @classmethod def lookup_country_code_from_ip(cls, ip): try: # Early check to avoid initializing GeoIP2 on invalid addresses if not ip: raise forms.ValidationError('No IP') validate_ipv46_address(ip) geoip = GeoIP2() value = geoip.country_code(ip) # Annoyingly, we have to catch both django's GeoIP2Exception (setup # issue) and geoip2's GeoIP2Error (lookup issue) except (forms.ValidationError, GeoIP2Exception, GeoIP2Error): value = '' return value @property def target(self): return self.addon or self.user @property def type(self): with translation.override(settings.LANGUAGE_CODE): type_ = (translation.ugettext(amo.ADDON_TYPE[self.addon.type]) if self.addon else 'User' if self.user else 'Addon') return type_ def __str__(self): name = self.target.name if self.target else self.guid return u'[%s] Abuse Report for %s' % (self.type, name)
class AbuseReport(ModelBase): # Note: those choices don't need to be translated for now, the # human-readable values are only exposed in the admin. ADDON_SIGNATURES = APIChoicesWithNone( ('CURATED_AND_PARTNER', 1, 'Curated and partner'), ('CURATED', 2, 'Curated'), ('PARTNER', 3, 'Partner'), ('NON_CURATED', 4, 'Non-curated'), ('UNSIGNED', 5, 'Unsigned'), ) REASONS = APIChoicesWithNone( ('HARMFUL', 1, 'Damages computer and/or data'), ('SPAM_OR_ADVERTISING', 2, 'Creates spam or advertising'), ('BROWSER_TAKEOVER', 3, 'Changes search / homepage / new tab page ' 'without informing user'), # `4` was previously 'New tab takeover' but has been merged into the # previous one. We avoid re-using the value. ('BROKEN', 5, "Doesn’t work, breaks websites, or slows Firefox down"), ('OFFENSIVE', 6, 'Hateful, violent, or illegal content'), ('DOES_NOT_MATCH_DESCRIPTION', 7, "Pretends to be something it’s not"), # `8` was previously "Doesn't work" but has been merged into the # previous one. We avoid re-using the value. ('UNWANTED', 9, "Wasn't wanted / impossible to get rid of"), ('OTHER', 10, 'Other'), ) ADDON_INSTALL_METHODS = APIChoicesWithNone( ('AMWEBAPI', 1, 'Add-on Manager Web API'), ('LINK', 2, 'Direct link'), ('INSTALLTRIGGER', 3, 'Install Trigger'), ('INSTALL_FROM_FILE', 4, 'From File'), ('MANAGEMENT_WEBEXT_API', 5, 'Webext management API'), ('DRAG_AND_DROP', 6, 'Drag & Drop'), ('SIDELOAD', 7, 'Sideload'), ) REPORT_ENTRY_POINTS = APIChoicesWithNone( ('UNINSTALL', 1, 'Uninstall'), ('MENU', 2, 'Menu'), ('TOOLBAR_CONTEXT_MENU', 3, 'Toolbar context menu'), ) # NULL if the reporter is anonymous. reporter = models.ForeignKey(UserProfile, null=True, blank=True, related_name='abuse_reported', on_delete=models.SET_NULL) country_code = models.CharField(max_length=2, default=None, null=True) # An abuse report can be for an addon or a user. # If user is non-null then both addon and guid should be null. # If user is null then addon should be non-null if guid was in our DB, # otherwise addon will be null also. # If both addon and user is null guid should be set. addon = models.ForeignKey(Addon, null=True, related_name='abuse_reports', on_delete=models.CASCADE) guid = models.CharField(max_length=255, null=True) user = models.ForeignKey(UserProfile, null=True, related_name='abuse_reports', on_delete=models.SET_NULL) message = models.TextField() # Extra optional fields for more information, giving some context that is # meant to be extracted automatically by the client (i.e. Firefox) and # submitted via the API. client_id = models.CharField(default=None, max_length=64, blank=True, null=True) addon_name = models.CharField(default=None, max_length=255, blank=True, null=True) addon_summary = models.CharField(default=None, max_length=255, blank=True, null=True) addon_version = models.CharField(default=None, max_length=255, blank=True, null=True) addon_signature = models.PositiveSmallIntegerField( default=None, choices=ADDON_SIGNATURES.choices, blank=True, null=True) application = models.PositiveSmallIntegerField(default=amo.FIREFOX.id, choices=amo.APPS_CHOICES, blank=True, null=True) application_version = models.CharField(default=None, max_length=255, blank=True, null=True) application_locale = models.CharField(default=None, max_length=255, blank=True, null=True) operating_system = models.CharField(default=None, max_length=255, blank=True, null=True) operating_system_version = models.CharField(default=None, max_length=255, blank=True, null=True) install_date = models.DateTimeField(default=None, blank=True, null=True) reason = models.PositiveSmallIntegerField(default=None, choices=REASONS.choices, blank=True, null=True) addon_install_origin = models.CharField(default=None, max_length=255, blank=True, null=True) addon_install_method = models.PositiveSmallIntegerField( default=None, choices=ADDON_INSTALL_METHODS.choices, blank=True, null=True) report_entry_point = models.PositiveSmallIntegerField( default=None, choices=REPORT_ENTRY_POINTS.choices, blank=True, null=True) class Meta: db_table = 'abuse_reports' def send(self): if self.reporter: user_name = '%s (%s)' % (self.reporter.name, self.reporter.email) else: user_name = 'An anonymous user' target_url = ('%s%s' % (settings.SITE_URL, self.target.get_url_path()) if self.target else 'GUID not in database') name = self.target.name if self.target else self.guid msg = u'%s reported abuse for %s (%s).\n\n%s' % ( user_name, name, target_url, self.message) send_mail(six.text_type(self), msg, recipient_list=(settings.ABUSE_EMAIL, )) def save(self, *args, **kwargs): creation = not self.pk super(AbuseReport, self).save(*args, **kwargs) if creation: self.send() @classmethod def lookup_country_code_from_ip(cls, ip): try: # Early check to avoid initializing GeoIP2 on invalid addresses if not ip: raise forms.ValidationError('No IP') validate_ipv46_address(ip) geoip = GeoIP2() value = geoip.country_code(ip) # Annoyingly, we have to catch both django's GeoIP2Exception (setup # issue) and geoip2's GeoIP2Error (lookup issue) except (forms.ValidationError, GeoIP2Exception, GeoIP2Error): value = '' return value @property def target(self): return self.addon or self.user @property def type(self): with translation.override(settings.LANGUAGE_CODE): type_ = (translation.ugettext(amo.ADDON_TYPE[self.addon.type]) if self.addon else 'User' if self.user else 'Addon') return type_ def __str__(self): name = self.target.name if self.target else self.guid return u'[%s] Abuse Report for %s' % (self.type, name)
class AbuseReport(ModelBase): # Note: those choices don't need to be translated for now, the # human-readable values are only exposed in the admin. ADDON_SIGNATURES = APIChoicesWithNone( ('CURATED_AND_PARTNER', 1, 'Curated and partner'), ('CURATED', 2, 'Curated'), ('PARTNER', 3, 'Partner'), ('NON_CURATED', 4, 'Non-curated'), ('UNSIGNED', 5, 'Unsigned'), ('BROKEN', 6, 'Broken'), ('UNKNOWN', 7, 'Unknown'), ('MISSING', 8, 'Missing'), ('PRELIMINARY', 9, 'Preliminary'), ('SIGNED', 10, 'Signed'), ('SYSTEM', 11, 'System'), ('PRIVILEGED', 12, 'Privileged'), ) REASONS = APIChoicesWithNone( ('DAMAGE', 1, 'Damages computer and/or data'), ('SPAM', 2, 'Creates spam or advertising'), ('SETTINGS', 3, 'Changes search / homepage / new tab page ' 'without informing user'), # `4` was previously 'New tab takeover' but has been merged into the # previous one. We avoid re-using the value. ('BROKEN', 5, "Doesn’t work, breaks websites, or slows Firefox down"), ('POLICY', 6, 'Hateful, violent, or illegal content'), ('DECEPTIVE', 7, "Pretends to be something it’s not"), # `8` was previously "Doesn't work" but has been merged into the # previous one. We avoid re-using the value. ('UNWANTED', 9, "Wasn't wanted / impossible to get rid of"), # `10` was previously "Other". We avoid re-using the value. ('OTHER', 127, 'Other'), ) ADDON_INSTALL_METHODS = APIChoicesWithNone( ('AMWEBAPI', 1, 'Add-on Manager Web API'), ('LINK', 2, 'Direct link'), ('INSTALLTRIGGER', 3, 'Install Trigger'), ('INSTALL_FROM_FILE', 4, 'From File'), ('MANAGEMENT_WEBEXT_API', 5, 'Webext management API'), ('DRAG_AND_DROP', 6, 'Drag & Drop'), ('SIDELOAD', 7, 'Sideload'), ('FILE_URL', 8, 'File URL'), ('ENTERPRISE_POLICY', 9, 'Enterprise Policy'), ('DISTRIBUTION', 10, 'Included in build'), ('SYSTEM_ADDON', 11, 'System Add-on'), ('TEMPORARY_ADDON', 12, 'Temporary Add-on'), ('SYNC', 13, 'Sync'), ('OTHER', 127, 'Other'), ) REPORT_ENTRY_POINTS = APIChoicesWithNone( ('UNINSTALL', 1, 'Uninstall'), ('MENU', 2, 'Menu'), ('TOOLBAR_CONTEXT_MENU', 3, 'Toolbar context menu'), ) STATES = Choices( ('UNTRIAGED', 1, 'Untriaged'), ('VALID', 2, 'Valid'), ('SUSPICIOUS', 3, 'Suspicious'), ('DELETED', 4, 'Deleted'), ) # NULL if the reporter is anonymous. reporter = models.ForeignKey(UserProfile, null=True, blank=True, related_name='abuse_reported', on_delete=models.SET_NULL) country_code = models.CharField(max_length=2, default=None, null=True) # An abuse report can be for an addon or a user. # If user is non-null then both addon and guid should be null. # If user is null then addon should be non-null if guid was in our DB, # otherwise addon will be null also. # If both addon and user is null guid should be set. addon = models.ForeignKey(Addon, null=True, related_name='abuse_reports', on_delete=models.CASCADE) guid = models.CharField(max_length=255, null=True) user = models.ForeignKey(UserProfile, null=True, related_name='abuse_reports', on_delete=models.SET_NULL) message = models.TextField(blank=True) state = models.PositiveSmallIntegerField(default=STATES.UNTRIAGED, choices=STATES.choices) # Extra optional fields for more information, giving some context that is # meant to be extracted automatically by the client (i.e. Firefox) and # submitted via the API. client_id = models.CharField(default=None, max_length=64, blank=True, null=True) addon_name = models.CharField(default=None, max_length=255, blank=True, null=True) addon_summary = models.CharField(default=None, max_length=255, blank=True, null=True) addon_version = models.CharField(default=None, max_length=255, blank=True, null=True) addon_signature = models.PositiveSmallIntegerField( default=None, choices=ADDON_SIGNATURES.choices, blank=True, null=True) application = models.PositiveSmallIntegerField(default=amo.FIREFOX.id, choices=amo.APPS_CHOICES, blank=True, null=True) application_version = models.CharField(default=None, max_length=255, blank=True, null=True) application_locale = models.CharField(default=None, max_length=255, blank=True, null=True) operating_system = models.CharField(default=None, max_length=255, blank=True, null=True) operating_system_version = models.CharField(default=None, max_length=255, blank=True, null=True) install_date = models.DateTimeField(default=None, blank=True, null=True) reason = models.PositiveSmallIntegerField(default=None, choices=REASONS.choices, blank=True, null=True) addon_install_origin = models.CharField(default=None, max_length=255, blank=True, null=True) addon_install_method = models.PositiveSmallIntegerField( default=None, choices=ADDON_INSTALL_METHODS.choices, blank=True, null=True) report_entry_point = models.PositiveSmallIntegerField( default=None, choices=REPORT_ENTRY_POINTS.choices, blank=True, null=True) unfiltered = AbuseReportManager(include_deleted=True) objects = AbuseReportManager() class Meta: db_table = 'abuse_reports' # See comment in addons/models.py about base_manager_name. It needs to # be unfiltered to prevent exceptions when dealing with relations or # saving already deleted objects. base_manager_name = 'unfiltered' def send(self): if self.reporter: user_name = '%s (%s)' % (self.reporter.name, self.reporter.email) else: user_name = 'An anonymous user' # Give a URL pointing to the admin for that report. If there is a # target (add-on or user in database) we can point directly to the # admin url for that object, otherwise we use the admin url of the # report itself. if self.target: target_url = self.target.get_admin_absolute_url() target_name = self.target.name else: target_url = self.get_admin_absolute_url() target_name = self.guid metadata = '\n'.join( ['%s => %s' % (k, v) for k, v in self.metadata.items()]) msg = '%s reported abuse for %s (%s).\n\n%s\n\n%s' % ( user_name, target_name, target_url, metadata, self.message) send_mail(str(self), msg, recipient_list=(settings.ABUSE_EMAIL, )) @property def metadata(self): """ Dict of metadata about this report. Only includes non-null values. """ data = {} field_names = ('client_id', 'addon_name', 'addon_summary', 'addon_version', 'addon_signature', 'application', 'application_version', 'application_locale', 'operating_system', 'operating_system_version', 'install_date', 'reason', 'addon_install_origin', 'addon_install_method', 'report_entry_point') for field_name in field_names: value = self.__dict__[field_name] # Only include values that matter. if value is not None: field = self._meta.get_field(field_name) # If it's a choice field, display the "pretty" version. if field.choices: value = getattr(self, 'get_%s_display' % field_name)() data[field_name] = value return data def save(self, *args, **kwargs): creation = not self.pk super(AbuseReport, self).save(*args, **kwargs) if creation: self.send() def delete(self, *args, **kwargs): # AbuseReports are soft-deleted. Note that we keep relations, because # the only possible relations are to users and add-ons, which are also # soft-deleted. return self.update(state=self.STATES.DELETED) @classmethod def lookup_country_code_from_ip(cls, ip): try: # Early check to avoid initializing GeoIP2 on invalid addresses if not ip: raise forms.ValidationError('No IP') validate_ipv46_address(ip) geoip = GeoIP2() value = geoip.country_code(ip) # Annoyingly, we have to catch both django's GeoIP2Exception (setup # issue) and geoip2's GeoIP2Error (lookup issue) except (forms.ValidationError, GeoIP2Exception, GeoIP2Error): value = '' return value @property def target(self): return self.addon or self.user @property def type(self): with translation.override(settings.LANGUAGE_CODE): if self.addon and self.addon.type in amo.ADDON_TYPE: type_ = (translation.ugettext(amo.ADDON_TYPE[self.addon.type])) elif self.user: type_ = 'User' else: type_ = 'Addon' return type_ def __str__(self): name = self.target.name if self.target else self.guid return u'[%s] Abuse Report for %s' % (self.type, name)