APPRISE_CONFIG_LOCK switch added for extra security (#57)

This commit is contained in:
Chris Caron
2021-11-06 17:21:41 -04:00
committed by GitHub
parent e65b80cb11
commit 2fcc5f43a9
11 changed files with 686 additions and 307 deletions

View File

@@ -23,6 +23,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from .utils import ConfigCache
from django.conf import settings
def stateful_mode(request):
@@ -30,3 +31,10 @@ def stateful_mode(request):
Returns our loaded Stateful Mode
"""
return {'STATEFUL_MODE': ConfigCache.mode}
def config_lock(request):
"""
Returns the state of our global configuration lock
"""
return {'CONFIG_LOCK': settings.APPRISE_CONFIG_LOCK}

View File

@@ -90,6 +90,7 @@ class AddByConfigForm(forms.Form):
label=_('Configuration'),
widget=forms.Textarea(),
max_length=4096,
required=False,
)
def clean_format(self):

View File

@@ -8,13 +8,14 @@
<ul class="tabs config-overview">
<li class="tab col s4"><a class="active" href="#overview"><i class="material-icons">info</i>
{% trans "Overview" %}</a></li>
<li class="tab col s4"><a href="#config"><i class="material-icons">settings</i> {%trans "Configuration" %}</a>
<li class="tab {% if CONFIG_LOCK %}tab-locked {% endif %}col s4"><a href="#config"><i class="material-icons">{% if not CONFIG_LOCK %}settings{% else %}lock{% endif %}</i> {%trans "Configuration" %}</a>
</li>
<li class="tab col s4"><a href="#notify"><i class="material-icons">announcement</i> {%trans "Notifications" %}</a>
</li>
</ul>
</div>
<div id="overview" class="col s12">
{% if not CONFIG_LOCK %}
<div class="section">
<h5>{% trans "Getting Started" %}</h5>
<ol>
@@ -27,8 +28,7 @@
{% blocktrans %}
You can always refer to the
<a href="https://github.com/caronc/apprise/wiki#notification-services">Apprise Wiki</a> if you're having
troubles
assembling your URL(s).
troubles assembling your URL(s).
{% endblocktrans %}
</li>
<li>
@@ -44,20 +44,40 @@
<div class="divider"></div>
<p><strong>
{% blocktrans %}To get started, the first thing you want to do is define your configuration. Do this by
clicking
on the <i>Configuration tab</i>.
clicking on the <i>Configuration tab</i>.
{% endblocktrans %}
</strong></p>
<div class="divider"></div>
</div>
{% else %}
<div class="section">
<h5>{% trans "Apprise Configuration is Locked" %}</h5>
<p>
{% blocktrans %}At this time, the administrator of this server has locked down all configuration. This means
That pre-created configuration is securely hidden for the purpose of notification transmission only.
New configuration can not be set, and existing configuration can not be modified or viewed.
{% endblocktrans %}</p>
</div>
{% endif %}
<div class="has-config">
<div class="section">
<h5>{% trans "Working With Your Configuration" %}</h5>
<p>
{% blocktrans %}The following command would cause apprise to directly notify all of your services:{% endblocktrans %}
<br />
<pre><code class="bash">apprise --body="Test Message" \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;apprise{% if request.is_secure %}s{% endif %}://{{request.META.HTTP_HOST}}{{BASE_URL}}/{{key}}?tags=all</code></pre>
</p>
{% if not CONFIG_LOCK %}
<p>
{% blocktrans %}The following command would cause apprise to retrieve the configuration loaded and
send a test notification to all of your added services:{% endblocktrans %}
<br />
<pre><code class="bash">apprise --body="Test Message" --tag=all \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;--config={{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/get/{{key}}</code></pre>
</p>
{% endif %}
</div>
<div class="section">
<h5>{% trans "Loaded Configuration" %}</h5>
@@ -69,6 +89,7 @@
</div>
</div>
<div id="config" class="col s12">
{% if not CONFIG_LOCK %}
<p>
{% blocktrans %}Define your configuration below:{% endblocktrans %}
<form id="addconfig" action="{% url "add" key %}" method="post">
@@ -78,7 +99,13 @@
</button>
</form>
</p>
{% else %}
<h5>{% trans "Your Configuration Is Locked" %}</h5>
<p>{% blocktrans %}Access to your configuration has been disabled by your administrator.{% endblocktrans %}
{% endif %}
</div>
<div id="notify" class="col s12">
<p>
{% blocktrans %}
@@ -102,7 +129,8 @@
{% endblock %}
{% block jsfooter %}
async function update() {
{% if STATEFUL_MODE != 'disabled' %}
async function main_init(){
// disable the notification tab until we know for certain
// a notification is possible
@@ -117,273 +145,322 @@ async function update() {
document.querySelector('#url-list').textContent = ''
document.querySelector('#url-list-progress').style.display = null;
{% if not CONFIG_LOCK %}
// Ensure no-config sections are visible
document.querySelector('.no-config')
.style.display = null;
{% endif %}
// perform our status check
let response = await fetch('{% url "get" key %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
// perform a tag retrieval; start with 'all'
let tags = ['all'];
let jsonResponse = await fetch('{% url "json_urls" key %}/?privacy=1', {
method: 'GET',
})
if(jsonResponse.status != 200) {
// Take an early exit
document.querySelector('#url-list-progress').style.display = 'none';
document.querySelector('#url-list').textContent = '{% trans "An error occurred retrieving the list of loaded Apprise URL(s)" %}'
return;
}
// Initialize our tags making it easy for an end user to
// choose from. Tags are based off ones found in the saved
// configuration.
const data = await jsonResponse.json();
let external_data = tags.concat(data.tags).reduce(function(result, item) {
result[item] = null;
return result;
}, {})
M.Chips.init(document.querySelectorAll('.chips'), {
placeholder: 'Optional Tag',
secondaryPlaceholder: 'Another Tag',
autocompleteOptions: {
data: external_data,
minLength: 0
},
onChipAdd: function(e, chip) {
var $this = this;
$this.chipsData.forEach(function(e, index) {
if(!(e.tag in external_data))
$this.deleteChip(index);
})
}
});
if(response.status == 204)
{
// no problem; we simply have no content to retrieve
return '';
}
else if(response.status == 200)
{
// configuration found
// Now build our our loaded list of configuration for our welcome page
let urlList = document.createElement('ul');
// Remove our restrictions on sending notifications
document.querySelector('.config-overview li a[href="#notify"]')
.parentNode.classList.remove('disabled');
// Create a list item for each url retrieved
data.urls.forEach(function (entry) {
let code = document.createElement('code');
let li = document.createElement('li');
code.textContent = entry.url;
li.setAttribute('class', 'card-panel');
li.appendChild(code);
// get our results
let result = await response.json();
// Get our tags associate with the URL
entry.tags.forEach(function (tag) {
let chip = document.createElement('div');
chip.setAttribute('class', 'chip');
chip.textContent = `🏷️ ${tag}`;
li.appendChild(chip);
});
// Set our configuration so it's visible
document.querySelector('#id_config').value = result.config;
// Set our format
document.querySelector('#id_format').value = result.format;
urlList.appendChild(li);
});
// dispatch our event to update our select box
if (typeof(Event) === 'function') {
var event = new Event('change');
} else { // for IE11
var event = document.createEvent('Event');
event.initEvent('change', true, true);
}
document.querySelector('#id_format').dispatchEvent(event);
// Store our new list
document.querySelector('#url-list-progress').style.display = 'none';
document.querySelector('#url-list').textContent = ''
if(urlList.childNodes.length > 0) {
// Ensure has-config sections are visible
document.querySelector('.has-config')
.style.display = null;
// Remove our restrictions on sending notifications
document.querySelector('.config-overview li a[href="#notify"]')
.parentNode.classList.remove('disabled');
{% if not CONFIG_LOCK %}
// Disable any no-config entries
document.querySelector('.no-config')
.style.display = 'none';
{% endif %}
// perform a tag retrieval; start with 'all'
let tags = ['all'];
// Save our list to the screen
document.querySelector('#url-list').appendChild(urlList);
let jsonResponse = fetch('{% url "json_urls" key %}?privacy=1', {
method: 'GET',
}).then(function(jsonResponse) {
return jsonResponse.json();
}).then(function (data) {
// Initialize our tags making it easy for an end user to
// choose from. Tags are based off ones found in the saved
// configuration.
let external_data = tags.concat(data.tags).reduce(function(result, item) {
result[item] = null;
return result;
}, {})
M.Chips.init(document.querySelectorAll('.chips'), {
placeholder: 'Optional Tag',
secondaryPlaceholder: 'Another Tag',
autocompleteOptions: {
data: external_data,
minLength: 0
},
onChipAdd: function(e, chip) {
var $this = this;
$this.chipsData.forEach(function(e, index) {
if(!(e.tag in external_data))
$this.deleteChip(index);
})
}
});
// Now build our our loaded list of configuration for our welcome page
let urlList = document.createElement('ul');
// Create a list item for each url retrieved
data.urls.forEach(function (entry) {
let code = document.createElement('code');
let li = document.createElement('li');
code.textContent = entry.url;
li.setAttribute('class', 'card-panel');
li.appendChild(code);
// Get our tags associate with the URL
entry.tags.forEach(function (tag) {
let chip = document.createElement('div');
chip.setAttribute('class', 'chip');
chip.textContent = `🏷️ ${tag}`;
li.appendChild(chip);
});
urlList.appendChild(li);
});
// Store our new list
document.querySelector('#url-list-progress').style.display = 'none';
document.querySelector('#url-list').textContent = ''
if(urlList.childNodes.length > 0) {
document.querySelector('#url-list').appendChild(urlList);
} else {
document.querySelector('#url-list').textContent = '{% trans "There are no Apprise URL(s) loaded." %}'
}
}).catch(function (err) {
// There was an error
document.querySelector('#url-list-progress').style.display = 'none';
document.querySelector('#url-list').textContent = '{% trans "An error occurred retrieving the list of loaded Apprise URL(s)" %}'
{% if not CONFIG_LOCK %}
//
// Load our configuration now into the configuration tab
//
let response = await fetch('{% url "get" key %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
});
return response;
if(response.status == 204)
{
// no problem; we simply have no content to retrieve
return '';
}
else if(response.status == 200)
{
// configuration found
// get our results
let result = await response.json();
// Set our configuration so it's visible
document.querySelector('#id_config').value = result.config;
// Set our format
document.querySelector('#id_format').value = result.format;
// dispatch our event to update our select box
if (typeof(Event) === 'function') {
var event = new Event('change');
} else { // for IE11
var event = document.createEvent('Event');
event.initEvent('change', true, true);
}
document.querySelector('#id_format').dispatchEvent(event);
}
{% endif %}
} else {
document.querySelector('#url-list').textContent = '{% trans "There are no Apprise URL(s) loaded." %}'
}
// if we reach here, we failed
return null;
}
update();
function config_init() {
// over-ride manual submit for a nicer user experience
document.querySelector('#addconfig').onsubmit = function(event) {
event.preventDefault();
const form = this;
const body = new URLSearchParams(new FormData(form));
// over-ride manual submit for a nicer user experience
document.querySelector('#addconfig').onsubmit = function(event) {
event.preventDefault();
const form = this;
const body = new URLSearchParams(new FormData(form));
content = document.querySelector('#id_config')
.value.replace(/^\s+|\s+$/gm,'');
if(content.length) {
// perform our status check
let response = fetch('{% url "add" key %}', {
method: 'POST',
body: body,
}).then(function(response) {
if(response.status == 200)
{
// update our settings
main_init();
// perform our status check
let response = fetch('{% url "add" key %}', {
method: 'POST',
body: body,
}).then(function(response) {
if(response.status == 200)
{
// update our settings
update();
// user notification
Swal.fire(
'{% trans "Save" %}',
'{% trans "Successfully saved the specified URL(s)." %}',
'success'
);
} else if(response.status == 500) {
// Disk issue
Swal.fire(
'{% trans "Save" %}',
'{% trans "There was an issue writing the configuration to disk. Check your file permissions and try again." %}',
'error'
);
} else {
// user notification
Swal.fire(
'{% trans "Save" %}',
'{% trans "Failed to save the specified URL(s). Check your syntax and try again." %}',
'error'
);
}
});
} else {
// Perform Delete
// perform our status check
let response = fetch('{% url "del" key %}', {
method: 'POST',
body: body,
}).then(function(response) {
if(response.status == 200 || response.status == 204)
{
// update our settings
main_init();
// user notification
Swal.fire(
'{% trans "Save" %}',
'{% trans "Successfully saved the specified URL(s)." %}',
'success'
);
} else if(response.status == 500) {
// Disk issue
Swal.fire(
'{% trans "Save" %}',
'{% trans "There was an issue writing the configuration to disk. Check your file permissions and try again." %}',
'error'
);
} else {
// user notification
Swal.fire(
'{% trans "Save" %}',
'{% trans "Failed to save the specified URL(s). Check your syntax and try again." %}',
'error'
);
}
});
return false;
}
// over-ride manual submit for a nicer user experience
document.querySelector('#donotify').onsubmit = function(event) {
event.preventDefault();
const chipElement = document.querySelector('.chips');
chipElement.querySelector('.chips');
const chipInput = document.querySelector('.chips > input');
if(chipInput.value) {
// This code just handles text typed in the tag section that was
// not submitted. This forces any lingering un-committed text
// into a tag just prior to it's submission
const ev = new KeyboardEvent('keydown', {
altKey:false,
bubbles: true,
cancelBubble: false,
cancelable: true,
charCode: 0,
code: "Enter",
composed: true,
ctrlKey: false,
currentTarget: null,
defaultPrevented: true,
detail: 0,
eventPhase: 0,
isComposing: false,
isTrusted: true,
key: "Enter",
keyCode: 13,
location: 0,
metaKey: false,
repeat: false,
returnValue: false,
shiftKey: false,
type: "keydown",
which: 13
});
chipInput.dispatchEvent(ev);
}
// store tags (as comma separated string) from materialize chip type into form
document.querySelector('#id_tag').value = M.Chips.getInstance(chipElement).chipsData.reduce(
function(s, a){
s.push(a.tag)
return s;
}, []).join(",")
const form = this;
const body = new URLSearchParams(new FormData(form));
// perform our notification
Swal.fire(
'{% trans "Notification" %}',
'{% trans "Sending notification(s)..." %}',
);
Swal.showLoading()
let response = fetch('{% url "notify" key %}', {
method: 'POST',
body: body,
headers: {
'Accept': 'text/html',
'X-Apprise-Log-Level': 'info'
// user notification
Swal.fire(
'{% trans "Delete" %}',
'{% trans "Successfully removed configuration." %}',
'success'
);
} else {
// user notification
Swal.fire(
'{% trans "Delete" %}',
'{% trans "There was an issue removing the configuration." %}',
'error'
);
}
});
}
}).then(function(response) {
response.text().then(function (html) {
if(response.status == 200)
{
// user notification
Swal.fire(
'{% trans "Notification" %}',
'{% trans "Successfully sent the notification(s)." %}' + html,
'success'
);
} else if(response.status == 424) {
// user notification
Swal.fire(
'{% trans "Notification" %}',
'{% trans "One or more of the notification(s) were not sent." %}' + html,
'warning'
);
} else {
// user notification
Swal.fire(
'{% trans "Notification" %}',
'{% trans "Failed to send the notification(s)." %}' + html,
'error'
);
}
});
});
return false;
return false;
}
}
function notify_init() {
// over-ride manual submit for a nicer user experience
document.querySelector('#donotify').onsubmit = function(event) {
event.preventDefault();
const chipElement = document.querySelector('.chips');
chipElement.querySelector('.chips');
const chipInput = document.querySelector('.chips > input');
if(chipInput.value) {
// This code just handles text typed in the tag section that was
// not submitted. This forces any lingering un-committed text
// into a tag just prior to it's submission
const ev = new KeyboardEvent('keydown', {
altKey:false,
bubbles: true,
cancelBubble: false,
cancelable: true,
charCode: 0,
code: "Enter",
composed: true,
ctrlKey: false,
currentTarget: null,
defaultPrevented: true,
detail: 0,
eventPhase: 0,
isComposing: false,
isTrusted: true,
key: "Enter",
keyCode: 13,
location: 0,
metaKey: false,
repeat: false,
returnValue: false,
shiftKey: false,
type: "keydown",
which: 13
});
chipInput.dispatchEvent(ev);
}
// store tags (as comma separated string) from materialize chip type into form
document.querySelector('#id_tag').value = M.Chips.getInstance(chipElement).chipsData.reduce(
function(s, a){
s.push(a.tag)
return s;
}, []).join(",")
const form = this;
const body = new URLSearchParams(new FormData(form));
// perform our notification
Swal.fire(
'{% trans "Notification" %}',
'{% trans "Sending notification(s)..." %}',
);
Swal.showLoading()
let response = fetch('{% url "notify" key %}', {
method: 'POST',
body: body,
headers: {
'Accept': 'text/html',
'X-Apprise-Log-Level': 'info'
}
}).then(function(response) {
response.text().then(function (html) {
if(response.status == 200)
{
// user notification
Swal.fire(
'{% trans "Notification" %}',
'{% trans "Successfully sent the notification(s)." %}' + html,
'success'
);
} else if(response.status == 424) {
// user notification
Swal.fire(
'{% trans "Notification" %}',
'{% trans "One or more of the notification(s) were not sent." %}' + html,
'warning'
);
} else {
// user notification
Swal.fire(
'{% trans "Notification" %}',
'{% trans "Failed to send the notification(s)." %}' + html,
'error'
);
}
});
});
return false;
}
}
/* Initialize our page */
main_init();
{% if not CONFIG_LOCK %}
/* Initialze our configuration */
config_init();
{% endif %}
notify_init();
{% endif %}
{% endblock %}
{% block onload %}
{% if STATEFUL_MODE != 'disabled' %}
{{ block.super }}
document.querySelector('label [for="id_tag"]')
{
// create a new div with the class 'chips' assigned to it
const element = document.createElement('div')
@@ -395,4 +472,5 @@ document.querySelector('label [for="id_tag"]')
// Hide tag field since we use the pretty Materialize Chip setup instead
document.querySelector('#id_tag').style.display = 'none';
{% endif %}
{% endblock %}

View File

@@ -25,6 +25,7 @@
from django.test import SimpleTestCase
from apprise import ConfigFormat
from unittest.mock import patch
from django.test.utils import override_settings
from ..forms import AUTO_DETECT_CONFIG_KEYWORD
import json
@@ -38,6 +39,19 @@ class AddTests(SimpleTestCase):
response = self.client.get('/add/**invalid-key**')
assert response.status_code == 404
@override_settings(APPRISE_CONFIG_LOCK=True)
def test_save_config_by_urls_with_lock(self):
"""
Test adding a configuration by URLs with lock set won't work
"""
# our key to use
key = 'test_save_config_by_urls_with_lock'
# We simply do not have permission to do so
response = self.client.post(
'/add/{}'.format(key), {'urls': 'mailto://user:pass@yahoo.ca'})
assert response.status_code == 403
def test_save_config_by_urls(self):
"""
Test adding an configuration by URLs
@@ -99,6 +113,22 @@ class AddTests(SimpleTestCase):
)
assert response.status_code == 200
# Test with JSON (and no payload provided)
response = self.client.post(
'/add/{}'.format(key),
data=json.dumps({}),
content_type='application/json',
)
assert response.status_code == 400
# Test with XML which simply isn't supported
response = self.client.post(
'/add/{}'.format(key),
data='<urls><url>mailto://user:pass@yahoo.ca</url></urls>',
content_type='application/xml',
)
assert response.status_code == 400
# Invalid JSON
response = self.client.post(
'/add/{}'.format(key),

View File

@@ -23,6 +23,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.test import SimpleTestCase
from django.test.utils import override_settings
from unittest.mock import patch
@@ -35,6 +36,18 @@ class DelTests(SimpleTestCase):
response = self.client.get('/del/**invalid-key**')
assert response.status_code == 404
@override_settings(APPRISE_CONFIG_LOCK=True)
def test_del_with_lock(self):
"""
Test deleting a configuration by URLs with lock set won't work
"""
# our key to use
key = 'test_delete_with_lock'
# We simply do not have permission to do so
response = self.client.post('/del/{}'.format(key))
assert response.status_code == 403
def test_del_post(self):
"""
Test DEL POST

View File

@@ -23,6 +23,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.test import SimpleTestCase
from django.test.utils import override_settings
from unittest.mock import patch
@@ -110,6 +111,23 @@ class JsonUrlsTests(SimpleTestCase):
assert 'tags' in response.json()['urls'][0]
assert len(response.json()['urls'][0]['tags']) == 0
# We can see that th URLs are not the same when the privacy flag is set
without_privacy = \
self.client.get('/json/urls/{}?privacy=1'.format(key))
with_privacy = self.client.get('/json/urls/{}'.format(key))
assert with_privacy.json()['urls'][0] != \
without_privacy.json()['urls'][0]
with override_settings(APPRISE_CONFIG_LOCK=True):
# When our configuration lock is set, our result set enforces the
# privacy flag even if it was otherwise set:
with_privacy = \
self.client.get('/json/urls/{}?privacy=1'.format(key))
# But now they're the same under this new condition
assert with_privacy.json()['urls'][0] == \
without_privacy.json()['urls'][0]
# Add a YAML file
response = self.client.post(
'/add/{}'.format(key), {

View File

@@ -23,6 +23,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.test import SimpleTestCase
from django.test.utils import override_settings
from unittest.mock import patch
from ..forms import NotifyForm
from ..utils import ConfigCache
@@ -36,6 +37,20 @@ class StatefulNotifyTests(SimpleTestCase):
Test stateless notifications
"""
@override_settings(APPRISE_CONFIG_LOCK=True)
def test_stateful_configuration_with_lock(self):
"""
Test the retrieval of configuration when the lock is set
"""
# our key to use
key = 'test_stateful_with_lock'
# It doesn't matter if there is or isn't any configuration; when this
# flag is set. All that overhead is skipped and we're denied access
# right off the bat
response = self.client.post('/get/{}'.format(key))
assert response.status_code == 403
@patch('apprise.Apprise.notify')
def test_stateful_configuration_io(self, mock_notify):
"""

View File

@@ -80,6 +80,7 @@ class ResponseCode(object):
okay = 200
no_content = 204
bad_request = 400
no_access = 403
not_found = 404
method_not_allowed = 405
failed_dependency = 424
@@ -125,6 +126,22 @@ class AddView(View):
"""
Handle a POST request
"""
# Detect the format our response should be in
json_response = MIME_IS_JSON.match(request.content_type) is not None
if settings.APPRISE_CONFIG_LOCK:
# General Access Control
msg = _('The site has been configured to deny this request.')
status = ResponseCode.no_access
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# our content
content = {}
if MIME_IS_FORM.match(request.content_type):
@@ -137,7 +154,7 @@ class AddView(View):
if form.is_valid():
content.update(form.cleaned_data)
elif MIME_IS_JSON.match(request.content_type):
elif json_response:
# Prepare our default response
try:
# load our JSON content
@@ -145,14 +162,26 @@ class AddView(View):
except (AttributeError, ValueError):
# could not parse JSON response...
return HttpResponse(
_('Invalid JSON specified.'),
status=ResponseCode.bad_request)
return JsonResponse({
'error': _('Invalid JSON specified.'),
},
encoder=JSONEncoder,
safe=False,
status=ResponseCode.bad_request,
)
if not content:
return HttpResponse(
_('The message format is not supported.'),
status=ResponseCode.bad_request)
# No information was posted
msg = _('The message format is not supported.')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# Create ourselves an apprise object to work with
a_obj = apprise.Apprise()
@@ -161,27 +190,45 @@ class AddView(View):
a_obj.add(content['urls'])
if not len(a_obj):
# No URLs were loaded
return HttpResponse(
_('No valid URLs were found.'),
status=ResponseCode.bad_request,
msg = _('No valid URLs were found.')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
if not ConfigCache.put(
key, '\r\n'.join([s.url() for s in a_obj]),
apprise.ConfigFormat.TEXT):
return HttpResponse(
_('The configuration could not be saved.'),
status=ResponseCode.internal_server_error,
msg = _('The configuration could not be saved.')
status = ResponseCode.internal_server_error
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
elif 'config' in content:
fmt = content.get('format', '').lower()
if fmt not in [i[0] for i in CONFIG_FORMATS]:
# Format must be one supported by apprise
return HttpResponse(
_('The format specified is invalid.'),
status=ResponseCode.bad_request,
msg = _('The format specified is invalid.')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# prepare our apprise config object
@@ -195,9 +242,15 @@ class AddView(View):
# Load our configuration
if not ac_obj.add_config(content['config'], format=fmt):
# The format could not be detected
return HttpResponse(
_('The configuration format could not be detected.'),
status=ResponseCode.bad_request,
msg = _('The configuration format could not be detected.')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# Add our configuration
@@ -206,24 +259,44 @@ class AddView(View):
if not len(a_obj):
# No specified URL(s) were loaded due to
# mis-configuration on the caller's part
return HttpResponse(
_('No valid URL(s) were specified.'),
status=ResponseCode.bad_request,
msg = _('No valid URL(s) were specified.')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
if not ConfigCache.put(
key, content['config'], fmt=ac_obj[0].config_format):
# Something went very wrong; return 500
return HttpResponse(
_('An error occured saving configuration.'),
status=ResponseCode.internal_server_error,
msg = _('An error occured saving configuration.')
status = ResponseCode.internal_server_error
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
else:
# No configuration specified; we're done
return HttpResponse(
_('No configuration specified.'),
status=ResponseCode.bad_request,
)
msg = _('No configuration specified.')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# If we reach here; we successfully loaded the configuration so we can
# go ahead and write it to disk and alert our caller of the success.
@@ -242,19 +315,47 @@ class DelView(View):
"""
Handle a POST request
"""
# Detect the format our response should be in
json_response = MIME_IS_JSON.match(request.content_type) is not None
if settings.APPRISE_CONFIG_LOCK:
# General Access Control
msg = _('The site has been configured to deny this request.')
status = ResponseCode.no_access
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# Clear the key
result = ConfigCache.clear(key)
if result is None:
return HttpResponse(
_('There was no configuration to remove.'),
status=ResponseCode.no_content,
msg = _('There was no configuration to remove.')
status = ResponseCode.no_content
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
elif result is False:
# There was a failure at the os level
return HttpResponse(
_('The configuration could not be removed.'),
status=ResponseCode.internal_server_error,
msg = _('The configuration could not be removed.')
status = ResponseCode.internal_server_error
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# Removed content
@@ -277,6 +378,20 @@ class GetView(View):
# Detect the format our response should be in
json_response = MIME_IS_JSON.match(request.content_type) is not None
if settings.APPRISE_CONFIG_LOCK:
# General Access Control
return HttpResponse(
_('The site has been configured to deny this request.'),
status=ResponseCode.no_access,
) if not json_response else JsonResponse({
'error':
_('The site has been configured to deny this request.')
},
encoder=JSONEncoder,
safe=False,
status=ResponseCode.no_access,
)
config, format = ConfigCache.get(key)
if config is None:
# The returned value of config and format tell a rather cryptic
@@ -299,15 +414,15 @@ class GetView(View):
)
# Something went very wrong; return 500
return HttpResponse(
_('An error occured accessing configuration.'),
status=ResponseCode.internal_server_error,
) if not json_response else JsonResponse({
'error': _('There was no configuration found.')
msg = _('An error occured accessing configuration.')
status = ResponseCode.internal_server_error
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=ResponseCode.internal_server_error,
status=status,
)
# Our configuration was retrieved; now our response varies on whether
@@ -342,6 +457,9 @@ class NotifyView(View):
"""
Handle a POST request
"""
# Detect the format our response should be in
json_response = MIME_IS_JSON.match(request.content_type) is not None
# our content
content = {}
if MIME_IS_FORM.match(request.content_type):
@@ -350,7 +468,7 @@ class NotifyView(View):
if form.is_valid():
content.update(form.cleaned_data)
elif MIME_IS_JSON.match(request.content_type):
elif json_response:
# Prepare our default response
try:
# load our JSON content
@@ -358,31 +476,54 @@ class NotifyView(View):
except (AttributeError, ValueError):
# could not parse JSON response...
return HttpResponse(
return JsonResponse(
_('Invalid JSON specified.'),
encoder=JSONEncoder,
safe=False,
status=ResponseCode.bad_request)
if not content:
# We could not handle the Content-Type
return HttpResponse(
_('The message format is not supported.'),
status=ResponseCode.bad_request)
msg = _('The message format is not supported.')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# Some basic error checking
if not content.get('body') or \
content.get('type', apprise.NotifyType.INFO) \
not in apprise.NOTIFY_TYPES:
return HttpResponse(
_('An invalid payload was specified.'),
status=ResponseCode.bad_request)
msg = _('An invalid payload was specified.')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# Acquire our body format (if identified)
body_format = content.get('format', apprise.NotifyFormat.TEXT)
if body_format and body_format not in apprise.NOTIFY_FORMATS:
return HttpResponse(
_('An invalid body input format was specified.'),
status=ResponseCode.bad_request)
msg = _('An invalid body input format was specified.')
status = ResponseCode.bad_request
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# If we get here, we have enough information to generate a notification
# with.
@@ -396,15 +537,27 @@ class NotifyView(View):
# config != None: we simply have no data
if format is not None:
# no content to return
return HttpResponse(
_('There was no configuration found.'),
status=ResponseCode.no_content,
msg = _('There was no configuration found.')
status = ResponseCode.no_content
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# Something went very wrong; return 500
return HttpResponse(
_('An error occured accessing configuration.'),
status=ResponseCode.internal_server_error,
msg = _('An error occured accessing configuration.')
status = ResponseCode.internal_server_error
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# Prepare ourselves a default Asset
@@ -493,11 +646,16 @@ class NotifyView(View):
if not result:
# If at least one notification couldn't be sent; change up
# the response to a 424 error code
return HttpResponse(
response if response is not None else
_('One or more notification could not be sent.'),
content_type=content_type,
status=ResponseCode.failed_dependency)
msg = _('One or more notification could not be sent.')
status = ResponseCode.failed_dependency
return HttpResponse(msg, status=status) \
if not json_response else JsonResponse({
'error': msg,
},
encoder=JSONEncoder,
safe=False,
status=status,
)
# Return our retrieved content
return HttpResponse(
@@ -633,8 +791,9 @@ class JsonUrlView(View):
# Privacy flag
# Support 'yes', '1', 'true', 'enable', 'active', and +
privacy = request.GET.get('privacy', 'no')[0] in (
'a', 'y', '1', 't', 'e', '+')
privacy = settings.APPRISE_CONFIG_LOCK or \
request.GET.get('privacy', 'no')[0] in (
'a', 'y', '1', 't', 'e', '+')
# Optionally filter on tags. Use comma to identify more then one
tag = request.GET.get('tag', 'all')