mirror of
https://github.com/caronc/apprise.git
synced 2024-11-21 23:53:23 +01:00
Added additional security for attachment handling (#300)
This commit is contained in:
parent
7187af5991
commit
fcd81160be
@ -58,13 +58,15 @@ class Apprise(object):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, servers=None, asset=None, debug=False):
|
||||
def __init__(self, servers=None, asset=None, location=None, debug=False):
|
||||
"""
|
||||
Loads a set of server urls while applying the Asset() module to each
|
||||
if specified.
|
||||
|
||||
If no asset is provided, then the default asset is used.
|
||||
|
||||
Optionally specify a global ContentLocation for a more strict means
|
||||
of handling Attachments.
|
||||
"""
|
||||
|
||||
# Initialize a server list of URLs
|
||||
@ -87,6 +89,11 @@ class Apprise(object):
|
||||
# Set our debug flag
|
||||
self.debug = debug
|
||||
|
||||
# Store our hosting location for optional strict rule handling
|
||||
# of Attachments. Setting this to None removes any attachment
|
||||
# restrictions.
|
||||
self.location = location
|
||||
|
||||
@staticmethod
|
||||
def instantiate(url, asset=None, tag=None, suppress_exceptions=True):
|
||||
"""
|
||||
@ -325,7 +332,8 @@ class Apprise(object):
|
||||
# Prepare attachments if required
|
||||
if attach is not None and not isinstance(attach, AppriseAttachment):
|
||||
try:
|
||||
attach = AppriseAttachment(attach, asset=self.asset)
|
||||
attach = AppriseAttachment(
|
||||
attach, asset=self.asset, location=self.location)
|
||||
|
||||
except TypeError:
|
||||
# bad attachments
|
||||
|
@ -29,6 +29,8 @@ from . import attachment
|
||||
from . import URLBase
|
||||
from .AppriseAsset import AppriseAsset
|
||||
from .logger import logger
|
||||
from .common import ContentLocation
|
||||
from .common import CONTENT_LOCATIONS
|
||||
from .utils import GET_SCHEMA_RE
|
||||
|
||||
|
||||
@ -38,7 +40,8 @@ class AppriseAttachment(object):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, paths=None, asset=None, cache=True, **kwargs):
|
||||
def __init__(self, paths=None, asset=None, cache=True, location=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Loads all of the paths/urls specified (if any).
|
||||
|
||||
@ -59,6 +62,25 @@ class AppriseAttachment(object):
|
||||
|
||||
It's also worth nothing that the cache value is only set to elements
|
||||
that are not already of subclass AttachBase()
|
||||
|
||||
Optionally set your current ContentLocation in the location argument.
|
||||
This is used to further handle attachments. The rules are as follows:
|
||||
- INACCESSIBLE: You simply have disabled use of the object; no
|
||||
attachments will be retrieved/handled.
|
||||
- HOSTED: You are hosting an attachment service for others.
|
||||
In these circumstances all attachments that are LOCAL
|
||||
based (such as file://) will not be allowed.
|
||||
- LOCAL: The least restrictive mode as local files can be
|
||||
referenced in addition to hosted.
|
||||
|
||||
In all both HOSTED and LOCAL modes, INACCESSIBLE attachment types will
|
||||
continue to be inaccessible. However if you set this field (location)
|
||||
to None (it's default value) the attachment location category will not
|
||||
be tested in any way (all attachment types will be allowed).
|
||||
|
||||
The location field is also a global option that can be set when
|
||||
initializing the Apprise object.
|
||||
|
||||
"""
|
||||
|
||||
# Initialize our attachment listings
|
||||
@ -71,6 +93,15 @@ class AppriseAttachment(object):
|
||||
self.asset = \
|
||||
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
||||
|
||||
if location is not None and location not in CONTENT_LOCATIONS:
|
||||
msg = "An invalid Attachment location ({}) was specified." \
|
||||
.format(location)
|
||||
logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Store our location
|
||||
self.location = location
|
||||
|
||||
# Now parse any paths specified
|
||||
if paths is not None:
|
||||
# Store our path(s)
|
||||
@ -123,26 +154,45 @@ class AppriseAttachment(object):
|
||||
|
||||
# Iterate over our attachments
|
||||
for _attachment in attachments:
|
||||
|
||||
if isinstance(_attachment, attachment.AttachBase):
|
||||
# Go ahead and just add our attachment into our list
|
||||
self.attachments.append(_attachment)
|
||||
if self.location == ContentLocation.INACCESSIBLE:
|
||||
logger.warning(
|
||||
"Attachments are disabled; ignoring {}"
|
||||
.format(_attachment))
|
||||
return_status = False
|
||||
continue
|
||||
|
||||
elif not isinstance(_attachment, six.string_types):
|
||||
if isinstance(_attachment, six.string_types):
|
||||
logger.debug("Loading attachment: {}".format(_attachment))
|
||||
# Instantiate ourselves an object, this function throws or
|
||||
# returns None if it fails
|
||||
instance = AppriseAttachment.instantiate(
|
||||
_attachment, asset=asset, cache=cache)
|
||||
if not isinstance(instance, attachment.AttachBase):
|
||||
return_status = False
|
||||
continue
|
||||
|
||||
elif not isinstance(_attachment, attachment.AttachBase):
|
||||
logger.warning(
|
||||
"An invalid attachment (type={}) was specified.".format(
|
||||
type(_attachment)))
|
||||
return_status = False
|
||||
continue
|
||||
|
||||
logger.debug("Loading attachment: {}".format(_attachment))
|
||||
else:
|
||||
# our entry is of type AttachBase, so just go ahead and point
|
||||
# our instance to it for some post processing below
|
||||
instance = _attachment
|
||||
|
||||
# Instantiate ourselves an object, this function throws or
|
||||
# returns None if it fails
|
||||
instance = AppriseAttachment.instantiate(
|
||||
_attachment, asset=asset, cache=cache)
|
||||
if not isinstance(instance, attachment.AttachBase):
|
||||
# Apply some simple logic if our location flag is set
|
||||
if self.location and ((
|
||||
self.location == ContentLocation.HOSTED
|
||||
and instance.location != ContentLocation.HOSTED)
|
||||
or instance.location == ContentLocation.INACCESSIBLE):
|
||||
logger.warning(
|
||||
"Attachment was disallowed due to accessibility "
|
||||
"restrictions ({}->{}): {}".format(
|
||||
self.location, instance.location,
|
||||
instance.url(privacy=True)))
|
||||
return_status = False
|
||||
continue
|
||||
|
||||
|
@ -41,8 +41,10 @@ from .common import OverflowMode
|
||||
from .common import OVERFLOW_MODES
|
||||
from .common import ConfigFormat
|
||||
from .common import CONFIG_FORMATS
|
||||
from .common import ConfigIncludeMode
|
||||
from .common import CONFIG_INCLUDE_MODES
|
||||
from .common import ContentIncludeMode
|
||||
from .common import CONTENT_INCLUDE_MODES
|
||||
from .common import ContentLocation
|
||||
from .common import CONTENT_LOCATIONS
|
||||
|
||||
from .URLBase import URLBase
|
||||
from .URLBase import PrivacyMode
|
||||
@ -69,6 +71,7 @@ __all__ = [
|
||||
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
|
||||
'NOTIFY_TYPES', 'NOTIFY_IMAGE_SIZES', 'NOTIFY_FORMATS', 'OVERFLOW_MODES',
|
||||
'ConfigFormat', 'CONFIG_FORMATS',
|
||||
'ConfigIncludeMode', 'CONFIG_INCLUDE_MODES',
|
||||
'ContentIncludeMode', 'CONTENT_INCLUDE_MODES',
|
||||
'ContentLocation', 'CONTENT_LOCATIONS',
|
||||
'PrivacyMode',
|
||||
]
|
||||
|
@ -28,6 +28,7 @@ import time
|
||||
import mimetypes
|
||||
from ..URLBase import URLBase
|
||||
from ..utils import parse_bool
|
||||
from ..common import ContentLocation
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
@ -62,6 +63,11 @@ class AttachBase(URLBase):
|
||||
# 5 MB = 5242880 bytes
|
||||
max_file_size = 5242880
|
||||
|
||||
# By default all attachments types are inaccessible.
|
||||
# Developers of items identified in the attachment plugin directory
|
||||
# are requried to set a location
|
||||
location = ContentLocation.INACCESSIBLE
|
||||
|
||||
# Here is where we define all of the arguments we accept on the url
|
||||
# such as: schema://whatever/?overflow=upstream&format=text
|
||||
# These act the same way as tokens except they are optional and/or
|
||||
|
@ -26,6 +26,7 @@
|
||||
import re
|
||||
import os
|
||||
from .AttachBase import AttachBase
|
||||
from ..common import ContentLocation
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
@ -40,6 +41,10 @@ class AttachFile(AttachBase):
|
||||
# The default protocol
|
||||
protocol = 'file'
|
||||
|
||||
# Content is local to the same location as the apprise instance
|
||||
# being called (server-side)
|
||||
location = ContentLocation.LOCAL
|
||||
|
||||
def __init__(self, path, **kwargs):
|
||||
"""
|
||||
Initialize Local File Attachment Object
|
||||
@ -81,6 +86,10 @@ class AttachFile(AttachBase):
|
||||
validate it.
|
||||
"""
|
||||
|
||||
if self.location == ContentLocation.INACCESSIBLE:
|
||||
# our content is inaccessible
|
||||
return False
|
||||
|
||||
# Ensure any existing content set has been invalidated
|
||||
self.invalidate()
|
||||
|
||||
|
@ -29,6 +29,7 @@ import six
|
||||
import requests
|
||||
from tempfile import NamedTemporaryFile
|
||||
from .AttachBase import AttachBase
|
||||
from ..common import ContentLocation
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
@ -50,6 +51,9 @@ class AttachHTTP(AttachBase):
|
||||
# The number of bytes in memory to read from the remote source at a time
|
||||
chunk_size = 8192
|
||||
|
||||
# Web based requests are remote/external to our current location
|
||||
location = ContentLocation.HOSTED
|
||||
|
||||
def __init__(self, headers=None, **kwargs):
|
||||
"""
|
||||
Initialize HTTP Object
|
||||
@ -86,6 +90,10 @@ class AttachHTTP(AttachBase):
|
||||
Perform retrieval of the configuration based on the specified request
|
||||
"""
|
||||
|
||||
if self.location == ContentLocation.INACCESSIBLE:
|
||||
# our content is inaccessible
|
||||
return False
|
||||
|
||||
# Ensure any existing content set has been invalidated
|
||||
self.invalidate()
|
||||
|
||||
|
@ -39,6 +39,7 @@ from . import AppriseConfig
|
||||
from .utils import parse_list
|
||||
from .common import NOTIFY_TYPES
|
||||
from .common import NOTIFY_FORMATS
|
||||
from .common import ContentLocation
|
||||
from .logger import logger
|
||||
|
||||
from . import __title__
|
||||
@ -224,8 +225,12 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||
|
||||
# Prepare our asset
|
||||
asset = AppriseAsset(
|
||||
# Our body format
|
||||
body_format=input_format,
|
||||
|
||||
# Set the theme
|
||||
theme=theme,
|
||||
|
||||
# Async mode is only used for Python v3+ and allows a user to send
|
||||
# all of their notifications asyncronously. This was made an option
|
||||
# incase there are problems in the future where it's better that
|
||||
@ -234,7 +239,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||
)
|
||||
|
||||
# Create our Apprise object
|
||||
a = Apprise(asset=asset, debug=debug)
|
||||
a = Apprise(asset=asset, debug=debug, location=ContentLocation.LOCAL)
|
||||
|
||||
# Load our configuration if no URLs or specified configuration was
|
||||
# identified on the command line
|
||||
|
@ -130,28 +130,58 @@ CONFIG_FORMATS = (
|
||||
)
|
||||
|
||||
|
||||
class ConfigIncludeMode(object):
|
||||
class ContentIncludeMode(object):
|
||||
"""
|
||||
The different Cofiguration inclusion modes. All Configuration
|
||||
plugins will have one of these associated with it.
|
||||
The different Content inclusion modes. All content based plugins will
|
||||
have one of these associated with it.
|
||||
"""
|
||||
# - Configuration inclusion of same type only; hence a file:// can include
|
||||
# - Content inclusion of same type only; hence a file:// can include
|
||||
# a file://
|
||||
# - Cross file inclusion is not allowed unless insecure_includes (a flag)
|
||||
# is set to True. In these cases STRICT acts as type ALWAYS
|
||||
STRICT = 'strict'
|
||||
|
||||
# This configuration type can never be included
|
||||
# This content type can never be included
|
||||
NEVER = 'never'
|
||||
|
||||
# File configuration can always be included
|
||||
# This content can always be included
|
||||
ALWAYS = 'always'
|
||||
|
||||
|
||||
CONFIG_INCLUDE_MODES = (
|
||||
ConfigIncludeMode.STRICT,
|
||||
ConfigIncludeMode.NEVER,
|
||||
ConfigIncludeMode.ALWAYS,
|
||||
CONTENT_INCLUDE_MODES = (
|
||||
ContentIncludeMode.STRICT,
|
||||
ContentIncludeMode.NEVER,
|
||||
ContentIncludeMode.ALWAYS,
|
||||
)
|
||||
|
||||
|
||||
class ContentLocation(object):
|
||||
"""
|
||||
This is primarily used for handling file attachments. The idea is
|
||||
to track the source of the attachment itself. We don't want
|
||||
remote calls to a server to access local attachments for example.
|
||||
|
||||
By knowing the attachment type and cross-associating it with how
|
||||
we plan on accessing the content, we can make a judgement call
|
||||
(for security reasons) if we will allow it.
|
||||
|
||||
Obviously local uses of apprise can access both local and remote
|
||||
type files.
|
||||
"""
|
||||
# Content is located locally (on the same server as apprise)
|
||||
LOCAL = 'local'
|
||||
|
||||
# Content is located in a remote location
|
||||
HOSTED = 'hosted'
|
||||
|
||||
# Content is inaccessible
|
||||
INACCESSIBLE = 'n/a'
|
||||
|
||||
|
||||
CONTENT_LOCATIONS = (
|
||||
ContentLocation.LOCAL,
|
||||
ContentLocation.HOSTED,
|
||||
ContentLocation.INACCESSIBLE,
|
||||
)
|
||||
|
||||
# This is a reserved tag that is automatically assigned to every
|
||||
|
@ -34,7 +34,7 @@ from ..AppriseAsset import AppriseAsset
|
||||
from ..URLBase import URLBase
|
||||
from ..common import ConfigFormat
|
||||
from ..common import CONFIG_FORMATS
|
||||
from ..common import ConfigIncludeMode
|
||||
from ..common import ContentIncludeMode
|
||||
from ..utils import GET_SCHEMA_RE
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
@ -65,7 +65,7 @@ class ConfigBase(URLBase):
|
||||
|
||||
# By default all configuration is not includable using the 'include'
|
||||
# line found in configuration files.
|
||||
allow_cross_includes = ConfigIncludeMode.NEVER
|
||||
allow_cross_includes = ContentIncludeMode.NEVER
|
||||
|
||||
# the config path manages the handling of relative include
|
||||
config_path = os.getcwd()
|
||||
@ -240,11 +240,11 @@ class ConfigBase(URLBase):
|
||||
|
||||
# Handle cross inclusion based on allow_cross_includes rules
|
||||
if (SCHEMA_MAP[schema].allow_cross_includes ==
|
||||
ConfigIncludeMode.STRICT
|
||||
ContentIncludeMode.STRICT
|
||||
and schema not in self.schemas()
|
||||
and not self.insecure_includes) or \
|
||||
SCHEMA_MAP[schema].allow_cross_includes == \
|
||||
ConfigIncludeMode.NEVER:
|
||||
ContentIncludeMode.NEVER:
|
||||
|
||||
# Prevent the loading if insecure base protocols
|
||||
ConfigBase.logger.warning(
|
||||
|
@ -28,7 +28,7 @@ import io
|
||||
import os
|
||||
from .ConfigBase import ConfigBase
|
||||
from ..common import ConfigFormat
|
||||
from ..common import ConfigIncludeMode
|
||||
from ..common import ContentIncludeMode
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ class ConfigFile(ConfigBase):
|
||||
protocol = 'file'
|
||||
|
||||
# Configuration file inclusion can only be of the same type
|
||||
allow_cross_includes = ConfigIncludeMode.STRICT
|
||||
allow_cross_includes = ContentIncludeMode.STRICT
|
||||
|
||||
def __init__(self, path, **kwargs):
|
||||
"""
|
||||
|
@ -28,7 +28,7 @@ import six
|
||||
import requests
|
||||
from .ConfigBase import ConfigBase
|
||||
from ..common import ConfigFormat
|
||||
from ..common import ConfigIncludeMode
|
||||
from ..common import ContentIncludeMode
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
@ -66,7 +66,7 @@ class ConfigHTTP(ConfigBase):
|
||||
max_error_buffer_size = 2048
|
||||
|
||||
# Configuration file inclusion can always include this type
|
||||
allow_cross_includes = ConfigIncludeMode.ALWAYS
|
||||
allow_cross_includes = ContentIncludeMode.ALWAYS
|
||||
|
||||
def __init__(self, headers=None, **kwargs):
|
||||
"""
|
||||
|
@ -33,6 +33,7 @@ from apprise.AppriseAsset import AppriseAsset
|
||||
from apprise.attachment.AttachBase import AttachBase
|
||||
from apprise.attachment import SCHEMA_MAP as ATTACH_SCHEMA_MAP
|
||||
from apprise.attachment import __load_matrix
|
||||
from apprise.common import ContentLocation
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
@ -183,11 +184,6 @@ def test_apprise_attachment():
|
||||
# Reset our object
|
||||
aa.clear()
|
||||
|
||||
# if instantiating attachments from the class, it will throw a TypeError
|
||||
# if attachments couldn't be loaded
|
||||
with pytest.raises(TypeError):
|
||||
AppriseAttachment('garbage://')
|
||||
|
||||
# Garbage in produces garbage out
|
||||
assert aa.add(None) is False
|
||||
assert aa.add(object()) is False
|
||||
@ -210,6 +206,39 @@ def test_apprise_attachment():
|
||||
# length remains unchanged
|
||||
assert len(aa) == 0
|
||||
|
||||
# if instantiating attachments from the class, it will throw a TypeError
|
||||
# if attachments couldn't be loaded
|
||||
with pytest.raises(TypeError):
|
||||
AppriseAttachment('garbage://')
|
||||
|
||||
# Load our other attachment types
|
||||
aa = AppriseAttachment(location=ContentLocation.LOCAL)
|
||||
|
||||
# Hosted type won't allow us to import files
|
||||
aa = AppriseAttachment(location=ContentLocation.HOSTED)
|
||||
assert len(aa) == 0
|
||||
|
||||
# Add our attachments defined a the head of this function
|
||||
aa.add(attachments)
|
||||
|
||||
# Our length is still zero because we can't import files in
|
||||
# a hosted environment
|
||||
assert len(aa) == 0
|
||||
|
||||
# Inaccessible type prevents the adding of new stuff
|
||||
aa = AppriseAttachment(location=ContentLocation.INACCESSIBLE)
|
||||
assert len(aa) == 0
|
||||
|
||||
# Add our attachments defined a the head of this function
|
||||
aa.add(attachments)
|
||||
|
||||
# Our length is still zero
|
||||
assert len(aa) == 0
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
# Invalid location specified
|
||||
AppriseAttachment(location="invalid")
|
||||
|
||||
# test cases when file simply doesn't exist
|
||||
aa = AppriseAttachment('file://non-existant-file.png')
|
||||
# Our length is still 1
|
||||
|
@ -30,7 +30,7 @@ import mock
|
||||
import pytest
|
||||
from apprise import NotifyFormat
|
||||
from apprise import ConfigFormat
|
||||
from apprise import ConfigIncludeMode
|
||||
from apprise import ContentIncludeMode
|
||||
from apprise.Apprise import Apprise
|
||||
from apprise.AppriseConfig import AppriseConfig
|
||||
from apprise.AppriseAsset import AppriseAsset
|
||||
@ -417,7 +417,7 @@ def test_apprise_config_instantiate():
|
||||
|
||||
class BadConfig(ConfigBase):
|
||||
# always allow incusion
|
||||
allow_cross_includes = ConfigIncludeMode.ALWAYS
|
||||
allow_cross_includes = ContentIncludeMode.ALWAYS
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(BadConfig, self).__init__(**kwargs)
|
||||
@ -450,7 +450,7 @@ def test_invalid_apprise_config(tmpdir):
|
||||
|
||||
class BadConfig(ConfigBase):
|
||||
# always allow incusion
|
||||
allow_cross_includes = ConfigIncludeMode.ALWAYS
|
||||
allow_cross_includes = ContentIncludeMode.ALWAYS
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(BadConfig, self).__init__(**kwargs)
|
||||
@ -699,7 +699,7 @@ def test_recursive_config_inclusion(tmpdir):
|
||||
protocol = 'always'
|
||||
|
||||
# Always type
|
||||
allow_cross_includes = ConfigIncludeMode.ALWAYS
|
||||
allow_cross_includes = ContentIncludeMode.ALWAYS
|
||||
|
||||
class ConfigCrossPostStrict(ConfigFile):
|
||||
"""
|
||||
@ -712,7 +712,7 @@ def test_recursive_config_inclusion(tmpdir):
|
||||
protocol = 'strict'
|
||||
|
||||
# Always type
|
||||
allow_cross_includes = ConfigIncludeMode.STRICT
|
||||
allow_cross_includes = ContentIncludeMode.STRICT
|
||||
|
||||
class ConfigCrossPostNever(ConfigFile):
|
||||
"""
|
||||
@ -725,7 +725,7 @@ def test_recursive_config_inclusion(tmpdir):
|
||||
protocol = 'never'
|
||||
|
||||
# Always type
|
||||
allow_cross_includes = ConfigIncludeMode.NEVER
|
||||
allow_cross_includes = ContentIncludeMode.NEVER
|
||||
|
||||
# store our entries
|
||||
CONFIG_SCHEMA_MAP['never'] = ConfigCrossPostNever
|
||||
|
@ -31,6 +31,7 @@ from os.path import join
|
||||
from apprise.attachment.AttachBase import AttachBase
|
||||
from apprise.attachment.AttachFile import AttachFile
|
||||
from apprise import AppriseAttachment
|
||||
from apprise.common import ContentLocation
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
@ -102,6 +103,23 @@ def test_attach_file():
|
||||
assert re.search(r'[?&]mime=', response.url()) is None
|
||||
assert re.search(r'[?&]name=', response.url()) is None
|
||||
|
||||
# Test case where location is simply set to INACCESSIBLE
|
||||
# Below is a bad example, but it proves the section of code properly works.
|
||||
# Ideally a server admin may wish to just disable all File based
|
||||
# attachments entirely. In this case, they simply just need to change the
|
||||
# global singleton at the start of their program like:
|
||||
#
|
||||
# import apprise
|
||||
# apprise.attachment.AttachFile.location = \
|
||||
# apprise.ContentLocation.INACCESSIBLE
|
||||
#
|
||||
response = AppriseAttachment.instantiate(path)
|
||||
assert isinstance(response, AttachFile)
|
||||
response.location = ContentLocation.INACCESSIBLE
|
||||
assert response.path is None
|
||||
# Downloads just don't work period
|
||||
assert response.download() is False
|
||||
|
||||
# File handling (even if image is set to maxium allowable)
|
||||
response = AppriseAttachment.instantiate(path)
|
||||
assert isinstance(response, AttachFile)
|
||||
@ -179,3 +197,7 @@ def test_attach_file():
|
||||
# We will match on mime type now (%2F = /)
|
||||
assert re.search(r'[?&]mime=image%2Fjpeg', response.url(), re.I)
|
||||
assert re.search(r'[?&]name=test\.jpeg', response.url(), re.I)
|
||||
|
||||
# Test hosted configuration and that we can't add a valid file
|
||||
aa = AppriseAttachment(location=ContentLocation.HOSTED)
|
||||
assert aa.add(path) is False
|
||||
|
@ -35,6 +35,7 @@ from apprise.attachment.AttachHTTP import AttachHTTP
|
||||
from apprise import AppriseAttachment
|
||||
from apprise.plugins.NotifyBase import NotifyBase
|
||||
from apprise.plugins import SCHEMA_MAP
|
||||
from apprise.common import ContentLocation
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
@ -228,6 +229,21 @@ def test_attach_http(mock_get):
|
||||
assert attachment
|
||||
assert len(attachment) == getsize(path)
|
||||
|
||||
# Test case where location is simply set to INACCESSIBLE
|
||||
# Below is a bad example, but it proves the section of code properly works.
|
||||
# Ideally a server admin may wish to just disable all HTTP based
|
||||
# attachments entirely. In this case, they simply just need to change the
|
||||
# global singleton at the start of their program like:
|
||||
#
|
||||
# import apprise
|
||||
# apprise.attachment.AttachHTTP.location = \
|
||||
# apprise.ContentLocation.INACCESSIBLE
|
||||
attachment = AttachHTTP(**results)
|
||||
attachment.location = ContentLocation.INACCESSIBLE
|
||||
assert attachment.path is None
|
||||
# Downloads just don't work period
|
||||
assert attachment.download() is False
|
||||
|
||||
# No path specified
|
||||
# No Content-Disposition specified
|
||||
# No filename (because no path)
|
||||
|
Loading…
Reference in New Issue
Block a user