utils.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import importlib
  2. from pathlib import Path
  3. from django.core.exceptions import ImproperlyConfigured, SuspiciousFileOperation
  4. from django.core.files.storage import default_storage
  5. from django.core.files.utils import validate_file_name
  6. from django.db import models
  7. from django.db.models import Q
  8. from taggit.managers import _TaggableManager
  9. from netbox.context import current_request
  10. from .constants import IMAGE_ATTACHMENT_IMAGE_FORMATS
  11. from .validators import CustomValidator
  12. __all__ = (
  13. 'SharedObjectViewMixin',
  14. 'filename_from_model',
  15. 'image_upload',
  16. 'is_report',
  17. 'is_script',
  18. 'is_taggable',
  19. 'run_validators',
  20. )
  21. class SharedObjectViewMixin:
  22. def get_queryset(self, request):
  23. """
  24. Return only shared objects, or those owned by the current user, unless this is a superuser.
  25. """
  26. queryset = super().get_queryset(request)
  27. if request.user.is_superuser:
  28. return queryset
  29. if request.user.is_anonymous:
  30. return queryset.filter(shared=True)
  31. return queryset.filter(
  32. Q(shared=True) | Q(user=request.user)
  33. )
  34. def filename_from_model(model: models.Model) -> str:
  35. """Standardizes how we generate filenames from model class for exports"""
  36. base = model._meta.verbose_name_plural.lower().replace(' ', '_')
  37. return f'netbox_{base}'
  38. def filename_from_object(context: dict) -> str:
  39. """Standardizes how we generate filenames from model class for exports"""
  40. if 'device' in context:
  41. base = f"{context['device'].name or 'config'}"
  42. elif 'virtualmachine' in context:
  43. base = f"{context['virtualmachine'].name or 'config'}"
  44. else:
  45. base = 'config'
  46. return base
  47. def is_taggable(obj):
  48. """
  49. Return True if the instance can have Tags assigned to it; False otherwise.
  50. """
  51. if hasattr(obj, 'tags'):
  52. if issubclass(obj.tags.__class__, _TaggableManager):
  53. return True
  54. return False
  55. def image_upload(instance, filename):
  56. """
  57. Return a path for uploading image attachments.
  58. - Normalizes browser paths (e.g., C:\\fake_path\\photo.jpg)
  59. - Uses the instance.name if provided (sanitized to a *basename*, no ext)
  60. - Prefixes with a machine-friendly identifier
  61. Note: Relies on Django's default_storage utility.
  62. """
  63. upload_dir = 'image-attachments'
  64. default_filename = 'unnamed'
  65. allowed_img_extensions = IMAGE_ATTACHMENT_IMAGE_FORMATS.keys()
  66. # Normalize Windows paths and create a Path object.
  67. normalized_filename = str(filename).replace('\\', '/')
  68. file_path = Path(normalized_filename)
  69. # Extract the extension from the uploaded file.
  70. ext = file_path.suffix.lower().lstrip('.')
  71. # Use the instance-provided name if available; otherwise use the file stem.
  72. # Rely on Django's get_valid_filename to perform sanitization.
  73. stem = (instance.name or file_path.stem).strip()
  74. try:
  75. safe_stem = default_storage.get_valid_name(stem)
  76. except SuspiciousFileOperation:
  77. safe_stem = default_filename
  78. # Append the uploaded extension only if it's an allowed image type
  79. final_name = f"{safe_stem}.{ext}" if ext in allowed_img_extensions else safe_stem
  80. # Create a machine-friendly prefix from the instance
  81. prefix = f"{instance.object_type.model}_{instance.object_id}"
  82. name_with_path = f"{upload_dir}/{prefix}_{final_name}"
  83. # Validate the generated relative path (blocks absolute/traversal)
  84. validate_file_name(name_with_path, allow_relative_path=True)
  85. return name_with_path
  86. def is_script(obj):
  87. """
  88. Returns True if the object is a Script or Report.
  89. """
  90. from .reports import Report
  91. from .scripts import Script
  92. try:
  93. return (issubclass(obj, Report) and obj != Report) or (issubclass(obj, Script) and obj != Script)
  94. except TypeError:
  95. return False
  96. def is_report(obj):
  97. """
  98. Returns True if the given object is a Report.
  99. """
  100. from .reports import Report
  101. try:
  102. return issubclass(obj, Report) and obj != Report
  103. except TypeError:
  104. return False
  105. def run_validators(instance, validators):
  106. """
  107. Run the provided iterable of CustomValidators for the instance.
  108. """
  109. request = current_request.get()
  110. for validator in validators:
  111. # Loading a validator class by a dotted path
  112. if type(validator) is str:
  113. module, cls = validator.rsplit('.', 1)
  114. validator = getattr(importlib.import_module(module), cls)()
  115. # Constructing a new instance on the fly from a ruleset
  116. elif type(validator) is dict:
  117. validator = CustomValidator(validator)
  118. elif not issubclass(validator.__class__, CustomValidator):
  119. raise ImproperlyConfigured(f"Invalid value for custom validator: {validator}")
  120. validator(instance, request)