mirror of
https://github.com/caronc/apprise.git
synced 2025-02-16 18:21:01 +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
|
Loads a set of server urls while applying the Asset() module to each
|
||||||
if specified.
|
if specified.
|
||||||
|
|
||||||
If no asset is provided, then the default asset is used.
|
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
|
# Initialize a server list of URLs
|
||||||
@ -87,6 +89,11 @@ class Apprise(object):
|
|||||||
# Set our debug flag
|
# Set our debug flag
|
||||||
self.debug = debug
|
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
|
@staticmethod
|
||||||
def instantiate(url, asset=None, tag=None, suppress_exceptions=True):
|
def instantiate(url, asset=None, tag=None, suppress_exceptions=True):
|
||||||
"""
|
"""
|
||||||
@ -325,7 +332,8 @@ class Apprise(object):
|
|||||||
# Prepare attachments if required
|
# Prepare attachments if required
|
||||||
if attach is not None and not isinstance(attach, AppriseAttachment):
|
if attach is not None and not isinstance(attach, AppriseAttachment):
|
||||||
try:
|
try:
|
||||||
attach = AppriseAttachment(attach, asset=self.asset)
|
attach = AppriseAttachment(
|
||||||
|
attach, asset=self.asset, location=self.location)
|
||||||
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# bad attachments
|
# bad attachments
|
||||||
|
@ -29,6 +29,8 @@ from . import attachment
|
|||||||
from . import URLBase
|
from . import URLBase
|
||||||
from .AppriseAsset import AppriseAsset
|
from .AppriseAsset import AppriseAsset
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
from .common import ContentLocation
|
||||||
|
from .common import CONTENT_LOCATIONS
|
||||||
from .utils import GET_SCHEMA_RE
|
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).
|
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
|
It's also worth nothing that the cache value is only set to elements
|
||||||
that are not already of subclass AttachBase()
|
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
|
# Initialize our attachment listings
|
||||||
@ -71,6 +93,15 @@ class AppriseAttachment(object):
|
|||||||
self.asset = \
|
self.asset = \
|
||||||
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
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
|
# Now parse any paths specified
|
||||||
if paths is not None:
|
if paths is not None:
|
||||||
# Store our path(s)
|
# Store our path(s)
|
||||||
@ -123,26 +154,45 @@ class AppriseAttachment(object):
|
|||||||
|
|
||||||
# Iterate over our attachments
|
# Iterate over our attachments
|
||||||
for _attachment in attachments:
|
for _attachment in attachments:
|
||||||
|
if self.location == ContentLocation.INACCESSIBLE:
|
||||||
if isinstance(_attachment, attachment.AttachBase):
|
logger.warning(
|
||||||
# Go ahead and just add our attachment into our list
|
"Attachments are disabled; ignoring {}"
|
||||||
self.attachments.append(_attachment)
|
.format(_attachment))
|
||||||
|
return_status = False
|
||||||
continue
|
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(
|
logger.warning(
|
||||||
"An invalid attachment (type={}) was specified.".format(
|
"An invalid attachment (type={}) was specified.".format(
|
||||||
type(_attachment)))
|
type(_attachment)))
|
||||||
return_status = False
|
return_status = False
|
||||||
continue
|
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
|
# Apply some simple logic if our location flag is set
|
||||||
# returns None if it fails
|
if self.location and ((
|
||||||
instance = AppriseAttachment.instantiate(
|
self.location == ContentLocation.HOSTED
|
||||||
_attachment, asset=asset, cache=cache)
|
and instance.location != ContentLocation.HOSTED)
|
||||||
if not isinstance(instance, attachment.AttachBase):
|
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
|
return_status = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -41,8 +41,10 @@ from .common import OverflowMode
|
|||||||
from .common import OVERFLOW_MODES
|
from .common import OVERFLOW_MODES
|
||||||
from .common import ConfigFormat
|
from .common import ConfigFormat
|
||||||
from .common import CONFIG_FORMATS
|
from .common import CONFIG_FORMATS
|
||||||
from .common import ConfigIncludeMode
|
from .common import ContentIncludeMode
|
||||||
from .common import CONFIG_INCLUDE_MODES
|
from .common import CONTENT_INCLUDE_MODES
|
||||||
|
from .common import ContentLocation
|
||||||
|
from .common import CONTENT_LOCATIONS
|
||||||
|
|
||||||
from .URLBase import URLBase
|
from .URLBase import URLBase
|
||||||
from .URLBase import PrivacyMode
|
from .URLBase import PrivacyMode
|
||||||
@ -69,6 +71,7 @@ __all__ = [
|
|||||||
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
|
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
|
||||||
'NOTIFY_TYPES', 'NOTIFY_IMAGE_SIZES', 'NOTIFY_FORMATS', 'OVERFLOW_MODES',
|
'NOTIFY_TYPES', 'NOTIFY_IMAGE_SIZES', 'NOTIFY_FORMATS', 'OVERFLOW_MODES',
|
||||||
'ConfigFormat', 'CONFIG_FORMATS',
|
'ConfigFormat', 'CONFIG_FORMATS',
|
||||||
'ConfigIncludeMode', 'CONFIG_INCLUDE_MODES',
|
'ContentIncludeMode', 'CONTENT_INCLUDE_MODES',
|
||||||
|
'ContentLocation', 'CONTENT_LOCATIONS',
|
||||||
'PrivacyMode',
|
'PrivacyMode',
|
||||||
]
|
]
|
||||||
|
@ -28,6 +28,7 @@ import time
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
from ..URLBase import URLBase
|
from ..URLBase import URLBase
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
|
from ..common import ContentLocation
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
@ -62,6 +63,11 @@ class AttachBase(URLBase):
|
|||||||
# 5 MB = 5242880 bytes
|
# 5 MB = 5242880 bytes
|
||||||
max_file_size = 5242880
|
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
|
# Here is where we define all of the arguments we accept on the url
|
||||||
# such as: schema://whatever/?overflow=upstream&format=text
|
# such as: schema://whatever/?overflow=upstream&format=text
|
||||||
# These act the same way as tokens except they are optional and/or
|
# These act the same way as tokens except they are optional and/or
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
from .AttachBase import AttachBase
|
from .AttachBase import AttachBase
|
||||||
|
from ..common import ContentLocation
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
@ -40,6 +41,10 @@ class AttachFile(AttachBase):
|
|||||||
# The default protocol
|
# The default protocol
|
||||||
protocol = 'file'
|
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):
|
def __init__(self, path, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize Local File Attachment Object
|
Initialize Local File Attachment Object
|
||||||
@ -81,6 +86,10 @@ class AttachFile(AttachBase):
|
|||||||
validate it.
|
validate it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if self.location == ContentLocation.INACCESSIBLE:
|
||||||
|
# our content is inaccessible
|
||||||
|
return False
|
||||||
|
|
||||||
# Ensure any existing content set has been invalidated
|
# Ensure any existing content set has been invalidated
|
||||||
self.invalidate()
|
self.invalidate()
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ import six
|
|||||||
import requests
|
import requests
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from .AttachBase import AttachBase
|
from .AttachBase import AttachBase
|
||||||
|
from ..common import ContentLocation
|
||||||
from ..URLBase import PrivacyMode
|
from ..URLBase import PrivacyMode
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
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
|
# The number of bytes in memory to read from the remote source at a time
|
||||||
chunk_size = 8192
|
chunk_size = 8192
|
||||||
|
|
||||||
|
# Web based requests are remote/external to our current location
|
||||||
|
location = ContentLocation.HOSTED
|
||||||
|
|
||||||
def __init__(self, headers=None, **kwargs):
|
def __init__(self, headers=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize HTTP Object
|
Initialize HTTP Object
|
||||||
@ -86,6 +90,10 @@ class AttachHTTP(AttachBase):
|
|||||||
Perform retrieval of the configuration based on the specified request
|
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
|
# Ensure any existing content set has been invalidated
|
||||||
self.invalidate()
|
self.invalidate()
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ from . import AppriseConfig
|
|||||||
from .utils import parse_list
|
from .utils import parse_list
|
||||||
from .common import NOTIFY_TYPES
|
from .common import NOTIFY_TYPES
|
||||||
from .common import NOTIFY_FORMATS
|
from .common import NOTIFY_FORMATS
|
||||||
|
from .common import ContentLocation
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
|
||||||
from . import __title__
|
from . import __title__
|
||||||
@ -224,8 +225,12 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
|||||||
|
|
||||||
# Prepare our asset
|
# Prepare our asset
|
||||||
asset = AppriseAsset(
|
asset = AppriseAsset(
|
||||||
|
# Our body format
|
||||||
body_format=input_format,
|
body_format=input_format,
|
||||||
|
|
||||||
|
# Set the theme
|
||||||
theme=theme,
|
theme=theme,
|
||||||
|
|
||||||
# Async mode is only used for Python v3+ and allows a user to send
|
# Async mode is only used for Python v3+ and allows a user to send
|
||||||
# all of their notifications asyncronously. This was made an option
|
# all of their notifications asyncronously. This was made an option
|
||||||
# incase there are problems in the future where it's better that
|
# 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
|
# 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
|
# Load our configuration if no URLs or specified configuration was
|
||||||
# identified on the command line
|
# identified on the command line
|
||||||
|
@ -130,28 +130,58 @@ CONFIG_FORMATS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigIncludeMode(object):
|
class ContentIncludeMode(object):
|
||||||
"""
|
"""
|
||||||
The different Cofiguration inclusion modes. All Configuration
|
The different Content inclusion modes. All content based plugins will
|
||||||
plugins will have one of these associated with it.
|
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://
|
# a file://
|
||||||
# - Cross file inclusion is not allowed unless insecure_includes (a flag)
|
# - Cross file inclusion is not allowed unless insecure_includes (a flag)
|
||||||
# is set to True. In these cases STRICT acts as type ALWAYS
|
# is set to True. In these cases STRICT acts as type ALWAYS
|
||||||
STRICT = 'strict'
|
STRICT = 'strict'
|
||||||
|
|
||||||
# This configuration type can never be included
|
# This content type can never be included
|
||||||
NEVER = 'never'
|
NEVER = 'never'
|
||||||
|
|
||||||
# File configuration can always be included
|
# This content can always be included
|
||||||
ALWAYS = 'always'
|
ALWAYS = 'always'
|
||||||
|
|
||||||
|
|
||||||
CONFIG_INCLUDE_MODES = (
|
CONTENT_INCLUDE_MODES = (
|
||||||
ConfigIncludeMode.STRICT,
|
ContentIncludeMode.STRICT,
|
||||||
ConfigIncludeMode.NEVER,
|
ContentIncludeMode.NEVER,
|
||||||
ConfigIncludeMode.ALWAYS,
|
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
|
# This is a reserved tag that is automatically assigned to every
|
||||||
|
@ -34,7 +34,7 @@ from ..AppriseAsset import AppriseAsset
|
|||||||
from ..URLBase import URLBase
|
from ..URLBase import URLBase
|
||||||
from ..common import ConfigFormat
|
from ..common import ConfigFormat
|
||||||
from ..common import CONFIG_FORMATS
|
from ..common import CONFIG_FORMATS
|
||||||
from ..common import ConfigIncludeMode
|
from ..common import ContentIncludeMode
|
||||||
from ..utils import GET_SCHEMA_RE
|
from ..utils import GET_SCHEMA_RE
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
@ -65,7 +65,7 @@ class ConfigBase(URLBase):
|
|||||||
|
|
||||||
# By default all configuration is not includable using the 'include'
|
# By default all configuration is not includable using the 'include'
|
||||||
# line found in configuration files.
|
# line found in configuration files.
|
||||||
allow_cross_includes = ConfigIncludeMode.NEVER
|
allow_cross_includes = ContentIncludeMode.NEVER
|
||||||
|
|
||||||
# the config path manages the handling of relative include
|
# the config path manages the handling of relative include
|
||||||
config_path = os.getcwd()
|
config_path = os.getcwd()
|
||||||
@ -240,11 +240,11 @@ class ConfigBase(URLBase):
|
|||||||
|
|
||||||
# Handle cross inclusion based on allow_cross_includes rules
|
# Handle cross inclusion based on allow_cross_includes rules
|
||||||
if (SCHEMA_MAP[schema].allow_cross_includes ==
|
if (SCHEMA_MAP[schema].allow_cross_includes ==
|
||||||
ConfigIncludeMode.STRICT
|
ContentIncludeMode.STRICT
|
||||||
and schema not in self.schemas()
|
and schema not in self.schemas()
|
||||||
and not self.insecure_includes) or \
|
and not self.insecure_includes) or \
|
||||||
SCHEMA_MAP[schema].allow_cross_includes == \
|
SCHEMA_MAP[schema].allow_cross_includes == \
|
||||||
ConfigIncludeMode.NEVER:
|
ContentIncludeMode.NEVER:
|
||||||
|
|
||||||
# Prevent the loading if insecure base protocols
|
# Prevent the loading if insecure base protocols
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
|
@ -28,7 +28,7 @@ import io
|
|||||||
import os
|
import os
|
||||||
from .ConfigBase import ConfigBase
|
from .ConfigBase import ConfigBase
|
||||||
from ..common import ConfigFormat
|
from ..common import ConfigFormat
|
||||||
from ..common import ConfigIncludeMode
|
from ..common import ContentIncludeMode
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ class ConfigFile(ConfigBase):
|
|||||||
protocol = 'file'
|
protocol = 'file'
|
||||||
|
|
||||||
# Configuration file inclusion can only be of the same type
|
# 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):
|
def __init__(self, path, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -28,7 +28,7 @@ import six
|
|||||||
import requests
|
import requests
|
||||||
from .ConfigBase import ConfigBase
|
from .ConfigBase import ConfigBase
|
||||||
from ..common import ConfigFormat
|
from ..common import ConfigFormat
|
||||||
from ..common import ConfigIncludeMode
|
from ..common import ContentIncludeMode
|
||||||
from ..URLBase import PrivacyMode
|
from ..URLBase import PrivacyMode
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ class ConfigHTTP(ConfigBase):
|
|||||||
max_error_buffer_size = 2048
|
max_error_buffer_size = 2048
|
||||||
|
|
||||||
# Configuration file inclusion can always include this type
|
# Configuration file inclusion can always include this type
|
||||||
allow_cross_includes = ConfigIncludeMode.ALWAYS
|
allow_cross_includes = ContentIncludeMode.ALWAYS
|
||||||
|
|
||||||
def __init__(self, headers=None, **kwargs):
|
def __init__(self, headers=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -33,6 +33,7 @@ from apprise.AppriseAsset import AppriseAsset
|
|||||||
from apprise.attachment.AttachBase import AttachBase
|
from apprise.attachment.AttachBase import AttachBase
|
||||||
from apprise.attachment import SCHEMA_MAP as ATTACH_SCHEMA_MAP
|
from apprise.attachment import SCHEMA_MAP as ATTACH_SCHEMA_MAP
|
||||||
from apprise.attachment import __load_matrix
|
from apprise.attachment import __load_matrix
|
||||||
|
from apprise.common import ContentLocation
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
import logging
|
import logging
|
||||||
@ -183,11 +184,6 @@ def test_apprise_attachment():
|
|||||||
# Reset our object
|
# Reset our object
|
||||||
aa.clear()
|
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
|
# Garbage in produces garbage out
|
||||||
assert aa.add(None) is False
|
assert aa.add(None) is False
|
||||||
assert aa.add(object()) is False
|
assert aa.add(object()) is False
|
||||||
@ -210,6 +206,39 @@ def test_apprise_attachment():
|
|||||||
# length remains unchanged
|
# length remains unchanged
|
||||||
assert len(aa) == 0
|
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
|
# test cases when file simply doesn't exist
|
||||||
aa = AppriseAttachment('file://non-existant-file.png')
|
aa = AppriseAttachment('file://non-existant-file.png')
|
||||||
# Our length is still 1
|
# Our length is still 1
|
||||||
|
@ -30,7 +30,7 @@ import mock
|
|||||||
import pytest
|
import pytest
|
||||||
from apprise import NotifyFormat
|
from apprise import NotifyFormat
|
||||||
from apprise import ConfigFormat
|
from apprise import ConfigFormat
|
||||||
from apprise import ConfigIncludeMode
|
from apprise import ContentIncludeMode
|
||||||
from apprise.Apprise import Apprise
|
from apprise.Apprise import Apprise
|
||||||
from apprise.AppriseConfig import AppriseConfig
|
from apprise.AppriseConfig import AppriseConfig
|
||||||
from apprise.AppriseAsset import AppriseAsset
|
from apprise.AppriseAsset import AppriseAsset
|
||||||
@ -417,7 +417,7 @@ def test_apprise_config_instantiate():
|
|||||||
|
|
||||||
class BadConfig(ConfigBase):
|
class BadConfig(ConfigBase):
|
||||||
# always allow incusion
|
# always allow incusion
|
||||||
allow_cross_includes = ConfigIncludeMode.ALWAYS
|
allow_cross_includes = ContentIncludeMode.ALWAYS
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(BadConfig, self).__init__(**kwargs)
|
super(BadConfig, self).__init__(**kwargs)
|
||||||
@ -450,7 +450,7 @@ def test_invalid_apprise_config(tmpdir):
|
|||||||
|
|
||||||
class BadConfig(ConfigBase):
|
class BadConfig(ConfigBase):
|
||||||
# always allow incusion
|
# always allow incusion
|
||||||
allow_cross_includes = ConfigIncludeMode.ALWAYS
|
allow_cross_includes = ContentIncludeMode.ALWAYS
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(BadConfig, self).__init__(**kwargs)
|
super(BadConfig, self).__init__(**kwargs)
|
||||||
@ -699,7 +699,7 @@ def test_recursive_config_inclusion(tmpdir):
|
|||||||
protocol = 'always'
|
protocol = 'always'
|
||||||
|
|
||||||
# Always type
|
# Always type
|
||||||
allow_cross_includes = ConfigIncludeMode.ALWAYS
|
allow_cross_includes = ContentIncludeMode.ALWAYS
|
||||||
|
|
||||||
class ConfigCrossPostStrict(ConfigFile):
|
class ConfigCrossPostStrict(ConfigFile):
|
||||||
"""
|
"""
|
||||||
@ -712,7 +712,7 @@ def test_recursive_config_inclusion(tmpdir):
|
|||||||
protocol = 'strict'
|
protocol = 'strict'
|
||||||
|
|
||||||
# Always type
|
# Always type
|
||||||
allow_cross_includes = ConfigIncludeMode.STRICT
|
allow_cross_includes = ContentIncludeMode.STRICT
|
||||||
|
|
||||||
class ConfigCrossPostNever(ConfigFile):
|
class ConfigCrossPostNever(ConfigFile):
|
||||||
"""
|
"""
|
||||||
@ -725,7 +725,7 @@ def test_recursive_config_inclusion(tmpdir):
|
|||||||
protocol = 'never'
|
protocol = 'never'
|
||||||
|
|
||||||
# Always type
|
# Always type
|
||||||
allow_cross_includes = ConfigIncludeMode.NEVER
|
allow_cross_includes = ContentIncludeMode.NEVER
|
||||||
|
|
||||||
# store our entries
|
# store our entries
|
||||||
CONFIG_SCHEMA_MAP['never'] = ConfigCrossPostNever
|
CONFIG_SCHEMA_MAP['never'] = ConfigCrossPostNever
|
||||||
|
@ -31,6 +31,7 @@ from os.path import join
|
|||||||
from apprise.attachment.AttachBase import AttachBase
|
from apprise.attachment.AttachBase import AttachBase
|
||||||
from apprise.attachment.AttachFile import AttachFile
|
from apprise.attachment.AttachFile import AttachFile
|
||||||
from apprise import AppriseAttachment
|
from apprise import AppriseAttachment
|
||||||
|
from apprise.common import ContentLocation
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
import logging
|
import logging
|
||||||
@ -102,6 +103,23 @@ def test_attach_file():
|
|||||||
assert re.search(r'[?&]mime=', response.url()) is None
|
assert re.search(r'[?&]mime=', response.url()) is None
|
||||||
assert re.search(r'[?&]name=', 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)
|
# File handling (even if image is set to maxium allowable)
|
||||||
response = AppriseAttachment.instantiate(path)
|
response = AppriseAttachment.instantiate(path)
|
||||||
assert isinstance(response, AttachFile)
|
assert isinstance(response, AttachFile)
|
||||||
@ -179,3 +197,7 @@ def test_attach_file():
|
|||||||
# We will match on mime type now (%2F = /)
|
# We will match on mime type now (%2F = /)
|
||||||
assert re.search(r'[?&]mime=image%2Fjpeg', response.url(), re.I)
|
assert re.search(r'[?&]mime=image%2Fjpeg', response.url(), re.I)
|
||||||
assert re.search(r'[?&]name=test\.jpeg', 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 import AppriseAttachment
|
||||||
from apprise.plugins.NotifyBase import NotifyBase
|
from apprise.plugins.NotifyBase import NotifyBase
|
||||||
from apprise.plugins import SCHEMA_MAP
|
from apprise.plugins import SCHEMA_MAP
|
||||||
|
from apprise.common import ContentLocation
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
import logging
|
import logging
|
||||||
@ -228,6 +229,21 @@ def test_attach_http(mock_get):
|
|||||||
assert attachment
|
assert attachment
|
||||||
assert len(attachment) == getsize(path)
|
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 path specified
|
||||||
# No Content-Disposition specified
|
# No Content-Disposition specified
|
||||||
# No filename (because no path)
|
# No filename (because no path)
|
||||||
|
Loading…
Reference in New Issue
Block a user