Browse Source

Remove tapi and replace by api

pull/1468/head
Safihre 5 years ago
parent
commit
51c9da73fe
  1. 2
      interfaces/Config/templates/_inc_header_uc.tmpl
  2. 6
      interfaces/Config/templates/config_general.tmpl
  3. 2
      interfaces/Config/templates/config_notify.tmpl
  4. 2
      interfaces/Config/templates/config_server.tmpl
  5. 6
      interfaces/Config/templates/config_sorting.tmpl
  6. 2
      interfaces/Config/templates/config_switches.tmpl
  7. 2
      interfaces/Glitter/templates/static/javascripts/glitter.basic.js
  8. 2
      interfaces/Glitter/templates/static/javascripts/glitter.main.js
  9. 2
      interfaces/Plush/templates/_inc_modals.tmpl
  10. 6
      interfaces/Plush/templates/static/javascripts/config.js
  11. 64
      interfaces/Plush/templates/static/javascripts/plush.js
  12. 2
      interfaces/wizard/static/javascript/checkserver.js
  13. 25
      sabnzbd/api.py
  14. 268
      sabnzbd/interface.py
  15. 2
      tests/testhelper.py

2
interfaces/Config/templates/_inc_header_uc.tmpl

@ -46,7 +46,7 @@
var sabSession = '$session'; var sabSession = '$session';
var rootURL = '${root}' var rootURL = '${root}'
var urlBase = '${url_base}' var urlBase = '${url_base}'
var folderBrowseUrl = '${root}tapi?mode=browse&output=json&apikey=$session'; var folderBrowseUrl = '${root}api?mode=browse&output=json&apikey=$session';
var folderSeperator = '#if $os.sep == '\\' then '\\\\' else '/'#' var folderSeperator = '#if $os.sep == '\\' then '\\\\' else '/'#'
// Translations // Translations

6
interfaces/Config/templates/config_general.tmpl

@ -289,7 +289,7 @@
if (confirm("$T('Plush-confirm')")) { if (confirm("$T('Plush-confirm')")) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "../../tapi", url: "../../api",
data: {mode:'config', name:'set_apikey', apikey: \$('#apikey').val()}, data: {mode:'config', name:'set_apikey', apikey: \$('#apikey').val()},
success: function(msg){ success: function(msg){
\$('#apikey').val(msg); \$('#apikey').val(msg);
@ -302,7 +302,7 @@
if (confirm("$T('Plush-confirm')")) { if (confirm("$T('Plush-confirm')")) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "../../tapi", url: "../../api",
data: { mode:'config', name:'set_nzbkey', apikey: \$('#apikey').val() }, data: { mode:'config', name:'set_nzbkey', apikey: \$('#apikey').val() },
success: function(msg){ success: function(msg){
\$('#nzbkey').val(msg); \$('#nzbkey').val(msg);
@ -334,7 +334,7 @@
// Submit request and then restart // Submit request and then restart
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "../../tapi", url: "../../api",
data: { mode: 'config', name: 'regenerate_certs', apikey: \$('#apikey').val() }, data: { mode: 'config', name: 'regenerate_certs', apikey: \$('#apikey').val() },
success: function(msg) { success: function(msg) {
do_restart() do_restart()

2
interfaces/Config/templates/config_notify.tmpl

@ -405,7 +405,7 @@
// Get the request // Get the request
\$.ajax({ \$.ajax({
type: "GET", type: "GET",
url: "../../tapi", url: "../../api",
data: data data: data
}).then(function(data) { }).then(function(data) {
// Remove disabled and make the box // Remove disabled and make the box

2
interfaces/Config/templates/config_server.tmpl

@ -491,7 +491,7 @@
theButton.find('span').toggleClass('glyphicon-sort glyphicon-refresh spin-glyphicon') theButton.find('span').toggleClass('glyphicon-sort glyphicon-refresh spin-glyphicon')
\$.ajax({ \$.ajax({
type: "POST", type: "POST",
url: "../../tapi", url: "../../api",
data: "mode=config&output=json&name=test_server&" + \$(this).parents('form:first').serialize() data: "mode=config&output=json&name=test_server&" + \$(this).parents('form:first').serialize()
}).then(function(data) { }).then(function(data) {
// Let's replace the link // Let's replace the link

6
interfaces/Config/templates/config_sorting.tmpl

@ -475,7 +475,7 @@
\$('#previewtv-result').addClass("loading"); \$('#previewtv-result').addClass("loading");
\$.ajax({ \$.ajax({
type: "GET", type: "GET",
url: "../../tapi", url: "../../api",
data: {mode:'eval_sort', value: 'series', name: \$('#tvsamplename').val(), title: \$tvsortstring, apikey: '$session', output: 'json' }, data: {mode:'eval_sort', value: 'series', name: \$('#tvsamplename').val(), title: \$tvsortstring, apikey: '$session', output: 'json' },
success: function(data){ success: function(data){
\$('#previewtv-result').removeClass("loading failure").html(data.result); \$('#previewtv-result').removeClass("loading failure").html(data.result);
@ -496,7 +496,7 @@
\$('#previewmovie-result').addClass("loading"); \$('#previewmovie-result').addClass("loading");
\$.ajax({ \$.ajax({
type: "GET", type: "GET",
url: "../../tapi", url: "../../api",
data: {mode:'eval_sort', value: 'movie', name: \$('#moviesamplename').val(), title: \$moviesortstring, movieextra: \$('#movieextra').val(), apikey: '$session', output: 'json' }, data: {mode:'eval_sort', value: 'movie', name: \$('#moviesamplename').val(), title: \$moviesortstring, movieextra: \$('#movieextra').val(), apikey: '$session', output: 'json' },
success: function(data){ success: function(data){
\$('#previewmovie-result').removeClass("loading failure").html(data.result); \$('#previewmovie-result').removeClass("loading failure").html(data.result);
@ -517,7 +517,7 @@
\$('#previewdate-result').addClass("loading"); \$('#previewdate-result').addClass("loading");
\$.ajax({ \$.ajax({
type: "GET", type: "GET",
url: "../../tapi", url: "../../api",
data: {mode:'eval_sort', value: 'date', name: \$('#datesamplename').val(), title: \$datesortstring, apikey: '$session', output: 'json' }, data: {mode:'eval_sort', value: 'date', name: \$('#datesamplename').val(), title: \$datesortstring, apikey: '$session', output: 'json' },
success: function(data){ success: function(data){
\$('#previewdate-result').removeClass("loading failure").html(data.result); \$('#previewdate-result').removeClass("loading failure").html(data.result);

2
interfaces/Config/templates/config_switches.tmpl

@ -535,7 +535,7 @@
// Send request // Send request
\$.ajax({ \$.ajax({
type: "GET", type: "GET",
url: "../../tapi", url: "../../api",
data: "mode=set_config_default&session=${session}&output=json&keyword=" + key_container.join('&keyword=') data: "mode=set_config_default&session=${session}&output=json&keyword=" + key_container.join('&keyword=')
}).then(function(data) { }).then(function(data) {
// Reload page // Reload page

2
interfaces/Glitter/templates/static/javascripts/glitter.basic.js

@ -30,7 +30,7 @@ function callAPI(data) {
data.output = "json"; data.output = "json";
data.apikey = apiKey; data.apikey = apiKey;
var ajaxQuery = $.ajax({ var ajaxQuery = $.ajax({
url: "./tapi", url: "./api",
type: "GET", type: "GET",
cache: false, cache: false,
data: data, data: data,

2
interfaces/Glitter/templates/static/javascripts/glitter.main.js

@ -717,7 +717,7 @@ function ViewModel() {
// Add this one // Add this one
$.ajax({ $.ajax({
url: "./tapi", url: "./api",
type: "POST", type: "POST",
cache: false, cache: false,
processData: false, processData: false,

2
interfaces/Plush/templates/_inc_modals.tmpl

@ -74,7 +74,7 @@ $T('Plush-containerWidth'):
<input type="text" id="addID_input" size="31" /> <input type="text" id="addID_input" size="31" />
<input type="submit" id="addID" value="$T('Plush-fetch')" class="juiButton" /> <input type="submit" id="addID" value="$T('Plush-fetch')" class="juiButton" />
<div id="add_nzb_hr"><hr></div> <div id="add_nzb_hr"><hr></div>
<form action="tapi" id="uploadNZBForm" method="post" enctype="multipart/form-data"> <form action="api" id="uploadNZBForm" method="post" enctype="multipart/form-data">
<input type="hidden" name="apikey" value="$session" /> <input type="hidden" name="apikey" value="$session" />
<input type="hidden" name="mode" value="addfile" /> <input type="hidden" name="mode" value="addfile" />
<input type="file" id="uploadNZBFile" name="name" size="12" style="width:180px;" /> <input type="file" id="uploadNZBFile" name="name" size="12" style="width:180px;" />

6
interfaces/Plush/templates/static/javascripts/config.js

@ -134,7 +134,7 @@ jQuery(document).ready(function($){
if (confirm($(this).attr('rel'))) { if (confirm($(this).attr('rel'))) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "../../tapi", url: "../../api",
data: {mode:'config', name:'set_apikey', apikey: $('#apikey').val()}, data: {mode:'config', name:'set_apikey', apikey: $('#apikey').val()},
success: function(msg){ success: function(msg){
$('#apikey,#session').val(msg); $('#apikey,#session').val(msg);
@ -147,7 +147,7 @@ jQuery(document).ready(function($){
if (confirm($(this).attr('rel'))) { if (confirm($(this).attr('rel'))) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "../../tapi", url: "../../api",
data: {mode:'config', name:'set_nzbkey', apikey: $('#apikey').val()}, data: {mode:'config', name:'set_nzbkey', apikey: $('#apikey').val()},
success: function(msg){ success: function(msg){
$('#nzbkey,#session').val(msg); $('#nzbkey,#session').val(msg);
@ -166,7 +166,7 @@ jQuery(document).ready(function($){
$(event.target).next('span').addClass('loading'); $(event.target).next('span').addClass('loading');
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "../../tapi", url: "../../api",
data: "mode=config&name=test_server&"+ $(event.target).parents('form:first').serialize() +"&apikey="+$('#apikey').val(), data: "mode=config&name=test_server&"+ $(event.target).parents('form:first').serialize() +"&apikey="+$('#apikey').val(),
success: function(msg){ success: function(msg){
alert(msg); alert(msg);

64
interfaces/Plush/templates/static/javascripts/plush.js

@ -43,7 +43,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: { data: {
mode: 'addurl', mode: 'addurl',
name: $("#addID_input").val(), name: $("#addID_input").val(),
@ -80,7 +80,7 @@ jQuery(function($){
/*$('#fetch_newzbin_bookmarks').click(function(){ /*$('#fetch_newzbin_bookmarks').click(function(){
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'newzbin', name:'get_bookmarks', apikey: $.plush.apikey}, data: {mode:'newzbin', name:'get_bookmarks', apikey: $.plush.apikey},
success: function(result){ success: function(result){
$.plush.RefreshQueue(); $.plush.RefreshQueue();
@ -190,7 +190,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'config', name:'set_speedlimit', value: speedLimit, apikey: $.plush.apikey} data: {mode:'config', name:'set_speedlimit', value: speedLimit, apikey: $.plush.apikey}
}); });
// Update // Update
@ -248,7 +248,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'change_complete_action', value: $(this).val(), apikey: $.plush.apikey} data: {mode:'queue', name:'change_complete_action', value: $(this).val(), apikey: $.plush.apikey}
}); });
}); });
@ -270,7 +270,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'delete', value:value, del_files:del_files, search: $('#queueSearchBox').val(), apikey: $.plush.apikey}, data: {mode:'queue', name:'delete', value:value, del_files:del_files, search: $('#queueSearchBox').val(), apikey: $.plush.apikey},
success: function(){ success: function(){
$.colorbox.close(); $.colorbox.close();
@ -291,7 +291,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'retry_all', apikey: $.plush.apikey}, data: {mode:'retry_all', apikey: $.plush.apikey},
success: function(){ success: function(){
$.colorbox.close(); $.colorbox.close();
@ -316,7 +316,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'sort', sort: sort, dir: dir, apikey: $.plush.apikey}, data: {mode:'queue', name:'sort', sort: sort, dir: dir, apikey: $.plush.apikey},
success: $.plush.RefreshQueue success: $.plush.RefreshQueue
}); });
@ -331,7 +331,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'config', name:'set_pause', value: minutes, apikey: $.plush.apikey}, data: {mode:'config', name:'set_pause', value: minutes, apikey: $.plush.apikey},
success: $.plush.RefreshQueue success: $.plush.RefreshQueue
}); });
@ -342,7 +342,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'newzbin', name:'get_bookmarks', apikey: $.plush.apikey}, data: {mode:'newzbin', name:'get_bookmarks', apikey: $.plush.apikey},
success: $.plush.RefreshQueue success: $.plush.RefreshQueue
}); });
@ -353,7 +353,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'reset_quota', apikey: $.plush.apikey}, data: {mode:'reset_quota', apikey: $.plush.apikey},
success: $.plush.RefreshQueue success: $.plush.RefreshQueue
}); });
@ -364,7 +364,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'rss_now', apikey: $.plush.apikey}, data: {mode:'rss_now', apikey: $.plush.apikey},
success: $.plush.RefreshQueue success: $.plush.RefreshQueue
}); });
@ -375,7 +375,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'watched_now', apikey: $.plush.apikey}, data: {mode:'watched_now', apikey: $.plush.apikey},
success: $.plush.RefreshQueue success: $.plush.RefreshQueue
}); });
@ -386,7 +386,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'resume_pp', apikey: $.plush.apikey}, data: {mode:'resume_pp', apikey: $.plush.apikey},
success: $.plush.RefreshQueue success: $.plush.RefreshQueue
}); });
@ -557,7 +557,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'resume', apikey: $.plush.apikey} data: {mode:'resume', apikey: $.plush.apikey}
}); });
} else { } else {
@ -566,7 +566,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'pause', apikey: $.plush.apikey} data: {mode:'pause', apikey: $.plush.apikey}
}); });
} }
@ -599,7 +599,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'pause', value: pid, apikey: $.plush.apikey} data: {mode:'queue', name:'pause', value: pid, apikey: $.plush.apikey}
}); });
} else { } else {
@ -607,7 +607,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'resume', value: pid, apikey: $.plush.apikey} data: {mode:'queue', name:'resume', value: pid, apikey: $.plush.apikey}
}); });
} }
@ -632,7 +632,7 @@ jQuery(function($){
$.plush.pendingQueueRefresh = true; $.plush.pendingQueueRefresh = true;
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'delete', value: delid, apikey: $.plush.apikey}, data: {mode:'queue', name:'delete', value: delid, apikey: $.plush.apikey},
success: function(){ success: function(){
if ( $("#queueTable tr:visible").length - 1 < 1 ) { // don't leave stranded on non-page if ( $("#queueTable tr:visible").length - 1 < 1 ) { // don't leave stranded on non-page
@ -652,7 +652,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'priority', value: nzbid, value2: $(this).val(), apikey: $.plush.apikey}, data: {mode:'queue', name:'priority', value: nzbid, value2: $(this).val(), apikey: $.plush.apikey},
success: function(newPos){ success: function(newPos){
// reposition the nzb if necessary (new position is returned by the API) // reposition the nzb if necessary (new position is returned by the API)
@ -675,7 +675,7 @@ jQuery(function($){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode: cval, value: val, value2: $(this).val(), apikey: $.plush.apikey}, data: {mode: cval, value: val, value2: $(this).val(), apikey: $.plush.apikey},
success: function(resp){ success: function(resp){
// each category can define different priority/processing/script -- must be accounted for // each category can define different priority/processing/script -- must be accounted for
@ -764,7 +764,7 @@ $.plush.queueprevslots = $.plush.queuenoofslots; // for the next refresh
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'switch', value: row.id, value2: val2, apikey: $.plush.apikey}, data: {mode:'switch', value: row.id, value2: val2, apikey: $.plush.apikey},
success: function(result){ success: function(result){
// change priority of the nzb if necessary (priority is returned by API) // change priority of the nzb if necessary (priority is returned by API)
@ -850,7 +850,7 @@ $("a","#multiops_inputs").click(function(e){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:$('#multi_status').val(), value: nzo_ids, apikey: $.plush.apikey} data: {mode:'queue', name:$('#multi_status').val(), value: nzo_ids, apikey: $.plush.apikey}
}); });
@ -858,7 +858,7 @@ $("a","#multiops_inputs").click(function(e){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode: 'change_cat', value: nzo_ids, value2: $('#multi_cat').val(), apikey: $.plush.apikey} data: {mode: 'change_cat', value: nzo_ids, value2: $('#multi_cat').val(), apikey: $.plush.apikey}
}); });
@ -866,7 +866,7 @@ $("a","#multiops_inputs").click(function(e){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'priority', value: nzo_ids, value2: $('#multi_priority').val(), apikey: $.plush.apikey} data: {mode:'queue', name:'priority', value: nzo_ids, value2: $('#multi_priority').val(), apikey: $.plush.apikey}
}); });
@ -874,7 +874,7 @@ $("a","#multiops_inputs").click(function(e){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode: 'change_opts', value: nzo_ids, value2: $('#multi_pp').val(), apikey: $.plush.apikey} data: {mode: 'change_opts', value: nzo_ids, value2: $('#multi_pp').val(), apikey: $.plush.apikey}
}); });
@ -882,7 +882,7 @@ $("a","#multiops_inputs").click(function(e){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode: 'change_script', value: nzo_ids, value2: $('#multi_script').val(), apikey: $.plush.apikey} data: {mode: 'change_script', value: nzo_ids, value2: $('#multi_script').val(), apikey: $.plush.apikey}
}); });
@ -913,7 +913,7 @@ $("a","#multiops_inputs").click(function(e){
if (!$.plush.confirmDeleteQueue || confirm($.plush.Tconfirmation)){ if (!$.plush.confirmDeleteQueue || confirm($.plush.Tconfirmation)){
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'delete', value: nzo_ids, apikey: $.plush.apikey}, data: {mode:'queue', name:'delete', value: nzo_ids, apikey: $.plush.apikey},
success: $.plush.RefreshQueue success: $.plush.RefreshQueue
}); });
@ -953,7 +953,7 @@ $("a","#multiops_inputs").click(function(e){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'history', name:'delete', value:value, del_files:del_files, search: $('#historySearchBox').val(), apikey: $.plush.apikey}, data: {mode:'history', name:'delete', value:value, del_files:del_files, search: $('#historySearchBox').val(), apikey: $.plush.apikey},
success: function(){ success: function(){
$.colorbox.close(); $.colorbox.close();
@ -1043,7 +1043,7 @@ $("a","#multiops_inputs").click(function(e){
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:mode, name:'delete', value: delid, del_files: del_files, apikey: $.plush.apikey}, data: {mode:mode, name:'delete', value: delid, del_files: del_files, apikey: $.plush.apikey},
success: function(){ success: function(){
if ( $("#historyTable tr:visible").length - 1 < 1 ) { // don't leave stranded on non-page if ( $("#historyTable tr:visible").length - 1 < 1 ) { // don't leave stranded on non-page
@ -1133,7 +1133,7 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'rating', value: nzo_id, type: videoAudio, setting: $(this).val(), apikey: $.plush.apikey}, data: {mode:'queue', name:'rating', value: nzo_id, type: videoAudio, setting: $(this).val(), apikey: $.plush.apikey},
success: $.plush.Refresh success: $.plush.Refresh
}); });
@ -1145,7 +1145,7 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'rating', value: nzo_id, type: 'vote', setting: upDown, apikey: $.plush.apikey}, data: {mode:'queue', name:'rating', value: nzo_id, type: 'vote', setting: upDown, apikey: $.plush.apikey},
success: $.plush.Refresh success: $.plush.Refresh
}); });
@ -1170,7 +1170,7 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
$.ajax({ $.ajax({
headers: {"Cache-Control": "no-cache"}, headers: {"Cache-Control": "no-cache"},
type: "POST", type: "POST",
url: "tapi", url: "api",
data: {mode:'queue', name:'rating', value: nzo_id, type: 'flag', setting: flag, detail: _detail, apikey: $.plush.apikey}, data: {mode:'queue', name:'rating', value: nzo_id, type: 'flag', setting: flag, detail: _detail, apikey: $.plush.apikey},
success: $.plush.RefreshHistory success: $.plush.RefreshHistory
}); });

2
interfaces/wizard/static/javascript/checkserver.js

@ -16,7 +16,7 @@ $(document).ready(function() {
$("#serverTest").click(function() { $("#serverTest").click(function() {
$('#serverResponse').html(txtChecking); $('#serverResponse').html(txtChecking);
$.getJSON( $.getJSON(
"../tapi?mode=config&name=test_server&output=json", "../api?mode=config&name=test_server&output=json",
$("form").serialize(), $("form").serialize(),
function(result) { function(result) {
if (result.value.result) { if (result.value.result) {

25
sabnzbd/api.py

@ -85,16 +85,35 @@ if os.name == 'nt':
else: else:
PATHEXT = [] PATHEXT = []
def api_handler(kwargs): def api_handler(kwargs):
""" API Dispatcher """ """ API Dispatcher """
if cfg.api_logging():
# Was it proxy forwarded?
xff = cherrypy.request.headers.get('X-Forwarded-For')
if xff:
logging.debug('API-call from %s (X-Forwarded-For: %s) [%s] %s', cherrypy.request.remote.ip,
xff, cherrypy.request.headers.get('User-Agent', '??'), kwargs)
else:
logging.debug('API-call from %s [%s] %s', cherrypy.request.remote.ip,
cherrypy.request.headers.get('User-Agent', '??'), kwargs)
# Clean-up the arguments
mode = kwargs.get('mode', '') mode = kwargs.get('mode', '')
output = kwargs.get('output')
name = kwargs.get('name', '') name = kwargs.get('name', '')
output = kwargs.get('output')
if isinstance(mode, list): if isinstance(mode, list):
mode = mode[0] mode = mode[0]
if isinstance(name, list):
name = name[0]
if isinstance(output, list): if isinstance(output, list):
output = output[0] output = output[0]
if mode not in ('version', 'auth'):
msg = sabnzbd.interface.check_apikey(kwargs)
if msg:
return report(output, msg)
response = _api_table.get(mode, (_api_undefined, 2))[0](name, output, kwargs) response = _api_table.get(mode, (_api_undefined, 2))[0](name, output, kwargs)
return response return response
@ -1531,7 +1550,7 @@ def build_header(webdir='', output=None, trans_functions=True):
header['power_options'] = sabnzbd.WIN32 or sabnzbd.DARWIN or sabnzbd.LINUX_POWER header['power_options'] = sabnzbd.WIN32 or sabnzbd.DARWIN or sabnzbd.LINUX_POWER
header['pp_pause_event'] = sabnzbd.scheduler.pp_pause_event() header['pp_pause_event'] = sabnzbd.scheduler.pp_pause_event()
header['session'] = cfg.api_key() header['apikey'] = cfg.api_key()
header['new_release'], header['new_rel_url'] = sabnzbd.NEW_VERSION header['new_release'], header['new_rel_url'] = sabnzbd.NEW_VERSION
header['version'] = sabnzbd.__version__ header['version'] = sabnzbd.__version__

268
sabnzbd/interface.py

@ -64,9 +64,8 @@ from sabnzbd.constants import MEBI, DEF_SKIN_COLORS, \
from sabnzbd.lang import list_languages from sabnzbd.lang import list_languages
from sabnzbd.api import list_scripts, list_cats, del_from_section, \ from sabnzbd.api import list_scripts, list_cats, del_from_section, \
api_handler, build_queue, build_status, retry_job, retry_all_jobs, \ api_handler, build_queue, build_status, retry_job, build_header, build_history, \
build_header, build_history, format_bytes, report, del_hist_job, Ttemplate, \ format_bytes, report, del_hist_job, Ttemplate, build_queue_header
build_queue_header
############################################################################## ##############################################################################
# Global constants # Global constants
@ -76,10 +75,10 @@ from sabnzbd.api import list_scripts, list_cats, del_from_section, \
############################################################################## ##############################################################################
# Security functions # Security functions
############################################################################## ##############################################################################
def secured_expose(wrap_func=None, check_configlock=False, check_session_key=False): def secured_expose(wrap_func=None, check_configlock=False, check_api_key=False):
""" Wrapper for both cherrypy.expose and login/access check """ """ Wrapper for both cherrypy.expose and login/access check """
if not wrap_func: if not wrap_func:
return functools.partial(secured_expose, check_configlock=check_configlock, check_session_key=check_session_key) return functools.partial(secured_expose, check_configlock=check_configlock, check_api_key=check_api_key)
# Expose to cherrypy # Expose to cherrypy
wrap_func.exposed = True wrap_func.exposed = True
@ -101,7 +100,7 @@ def secured_expose(wrap_func=None, check_configlock=False, check_session_key=Fal
return 'Access denied' return 'Access denied'
# Verify login status, only for non-key pages # Verify login status, only for non-key pages
if not check_login() and not check_session_key: if not check_login() and not check_api_key:
raise Raiser('/login/') raise Raiser('/login/')
# Verify host used for the visit # Verify host used for the visit
@ -109,9 +108,9 @@ def secured_expose(wrap_func=None, check_configlock=False, check_session_key=Fal
cherrypy.response.status = 403 cherrypy.response.status = 403
return 'Access denied - Hostname verification failed: https://sabnzbd.org/hostname-check' return 'Access denied - Hostname verification failed: https://sabnzbd.org/hostname-check'
# Some pages need correct session key # Some pages need correct API key
if check_session_key: if check_api_key:
msg = check_session(kwargs) msg = check_apikey(kwargs)
if msg: if msg:
return msg return msg
@ -250,65 +249,46 @@ def set_auth(conf):
conf.update({'tools.auth_basic.on': False}) conf.update({'tools.auth_basic.on': False})
def check_session(kwargs): def check_apikey(kwargs):
""" Check session key """ """ Check API-key or NZB-key
if not check_access():
return 'Access denied'
key = kwargs.get('session')
if not key:
key = kwargs.get('apikey')
msg = None
if not key:
log_warning_and_ip(T('Missing Session key'))
msg = T('Error: Session Key Required')
elif key != cfg.api_key():
log_warning_and_ip(T('Error: Session Key Incorrect'))
msg = T('Error: Session Key Incorrect')
return msg
def check_apikey(kwargs, nokey=False):
""" Check api key or nzbkey
Return None when OK, otherwise an error message Return None when OK, otherwise an error message
""" """
output = kwargs.get('output') output = kwargs.get('output')
mode = kwargs.get('mode', '') mode = kwargs.get('mode', '')
name = kwargs.get('name', '') name = kwargs.get('name', '')
# Lookup required access level # Lookup required access level, returns 4 for config-things
req_access = sabnzbd.api.api_level(mode, name) req_access = sabnzbd.api.api_level(mode, name)
if req_access == 1 and check_access(1): if req_access == 1 and check_access(1):
# NZB-only actions # NZB-only actions
pass pass
elif not check_access(req_access): elif not check_access(req_access):
return report(output, 'Access denied') return 'Access denied'
# First check APIKEY, if OK that's sufficient # First check API-key, if OK that's sufficient
if not (cfg.disable_key() or nokey): if not cfg.disable_key():
key = kwargs.get('apikey') key = kwargs.get('apikey')
if not key: if not key:
key = kwargs.get('session')
if not key:
if cfg.api_warnings(): if cfg.api_warnings():
log_warning_and_ip(T('API Key missing, please enter the api key from Config->General into your 3rd party program:')) log_warning_and_ip(T('API Key missing, please enter the api key from Config->General into your 3rd party program:'))
return report(output, 'API Key Required') return 'API Key Required'
elif req_access == 1 and key == cfg.nzb_key(): elif req_access == 1 and key == cfg.nzb_key():
return None return None
elif key == cfg.api_key(): elif key == cfg.api_key():
return None return None
else: else:
log_warning_and_ip(T('API Key incorrect, Use the api key from Config->General in your 3rd party program:')) log_warning_and_ip(T('API Key incorrect, Use the api key from Config->General in your 3rd party program:'))
return report(output, 'API Key Incorrect') return 'API Key Incorrect'
# No active APIKEY, check web credentials instead # No active API-key, check web credentials instead
if cfg.username() and cfg.password(): if cfg.username() and cfg.password():
if check_login() or (kwargs.get('ma_username') == cfg.username() and kwargs.get('ma_password') == cfg.password()): if check_login() or (kwargs.get('ma_username') == cfg.username() and kwargs.get('ma_password') == cfg.password()):
pass pass
else: else:
if cfg.api_warnings(): if cfg.api_warnings():
log_warning_and_ip(T('Authentication missing, please enter username/password from Config->General into your 3rd party program:')) log_warning_and_ip(T('Authentication missing, please enter username/password from Config->General into your 3rd party program:'))
return report(output, 'Missing authentication') return 'Missing authentication'
return None return None
@ -396,7 +376,7 @@ class MainPage:
# Redirect to the setup wizard # Redirect to the setup wizard
raise cherrypy.HTTPRedirect('%s/wizard/' % cfg.url_base()) raise cherrypy.HTTPRedirect('%s/wizard/' % cfg.url_base())
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def shutdown(self, **kwargs): def shutdown(self, **kwargs):
# Check for PID # Check for PID
pid_in = kwargs.get('pid') pid_in = kwargs.get('pid')
@ -406,55 +386,26 @@ class MainPage:
sabnzbd.shutdown_program() sabnzbd.shutdown_program()
return T('SABnzbd shutdown finished') return T('SABnzbd shutdown finished')
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def pause(self, **kwargs): def pause(self, **kwargs):
scheduler.plan_resume(0) scheduler.plan_resume(0)
Downloader.do.pause() Downloader.do.pause()
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def resume(self, **kwargs): def resume(self, **kwargs):
scheduler.plan_resume(0) scheduler.plan_resume(0)
sabnzbd.unpause_all() sabnzbd.unpause_all()
raise Raiser(self.__root) raise Raiser(self.__root)
@cherrypy.expose @cherrypy.expose
def tapi(self, **kwargs):
""" Handler for API over http, for template use """
msg = check_apikey(kwargs)
if msg:
return msg
return api_handler(kwargs)
@cherrypy.expose
def api(self, **kwargs): def api(self, **kwargs):
""" Handler for API over http, with explicit authentication parameters """ """ Redirect to API-handler """
if cfg.api_logging():
# Was it proxy forwarded?
xff = cherrypy.request.headers.get('X-Forwarded-For')
if xff:
logging.debug('API-call from %s (X-Forwarded-For: %s) [%s] %s', cherrypy.request.remote.ip,
xff, cherrypy.request.headers.get('User-Agent', '??'), kwargs)
else:
logging.debug('API-call from %s [%s] %s', cherrypy.request.remote.ip,
cherrypy.request.headers.get('User-Agent', '??'), kwargs)
mode = kwargs.get('mode', '')
if isinstance(mode, list):
mode = mode[0]
kwargs['mode'] = mode
name = kwargs.get('name', '')
if isinstance(name, list):
name = name[0]
kwargs['name'] = name
if mode not in ('version', 'auth'):
msg = check_apikey(kwargs)
if msg:
return msg
return api_handler(kwargs) return api_handler(kwargs)
@secured_expose @secured_expose
def scriptlog(self, **kwargs): def scriptlog(self, **kwargs):
""" Duplicate of scriptlog of History, needed for some skins """ """ Needed for all skins, URL is fixed due to postproc """
# No session key check, due to fixed URLs # No session key check, due to fixed URLs
name = kwargs.get('name') name = kwargs.get('name')
if name: if name:
@ -463,7 +414,7 @@ class MainPage:
else: else:
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def retry(self, **kwargs): def retry(self, **kwargs):
""" Duplicate of retry of History, needed for some skins """ """ Duplicate of retry of History, needed for some skins """
job = kwargs.get('job', '') job = kwargs.get('job', '')
@ -476,7 +427,7 @@ class MainPage:
del_hist_job(job, del_files=True) del_hist_job(job, del_files=True)
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def retry_pp(self, **kwargs): def retry_pp(self, **kwargs):
# Duplicate of History/retry_pp to please the SMPL skin :( # Duplicate of History/retry_pp to please the SMPL skin :(
retry_job(kwargs.get('job'), kwargs.get('nzbfile'), kwargs.get('password')) retry_job(kwargs.get('job'), kwargs.get('nzbfile'), kwargs.get('password'))
@ -749,7 +700,7 @@ class NzoPage:
return template.respond() return template.respond()
else: else:
# Job no longer exists, go to main page # Job no longer exists, go to main page
raise Raiser(cherrypy.lib.httputil.urljoin(self.__root, '../queue/')) raise Raiser(urllib.parse.urljoin(self.__root, '../queue/'))
def nzo_details(self, info, pnfo_list, nzo_id): def nzo_details(self, info, pnfo_list, nzo_id):
slot = {} slot = {}
@ -849,7 +800,7 @@ class NzoPage:
if priority is not None and nzo.priority != int(priority): if priority is not None and nzo.priority != int(priority):
NzbQueue.do.set_priority(nzo_id, priority) NzbQueue.do.set_priority(nzo_id, priority)
raise Raiser(cherrypy.lib.httputil.urljoin(self.__root, '../queue/')) raise Raiser(urllib.parse.urljoin(self.__root, '../queue/'))
def bulk_operation(self, nzo_id, kwargs): def bulk_operation(self, nzo_id, kwargs):
self.__cached_selection = kwargs self.__cached_selection = kwargs
@ -874,9 +825,9 @@ class NzoPage:
NzbQueue.do.move_bottom_bulk(nzo_id, nzf_ids) NzbQueue.do.move_bottom_bulk(nzo_id, nzf_ids)
if NzbQueue.do.get_nzo(nzo_id): if NzbQueue.do.get_nzo(nzo_id):
url = cherrypy.lib.httputil.urljoin(self.__root, nzo_id) url = urllib.parse.urljoin(self.__root, nzo_id)
else: else:
url = cherrypy.lib.httputil.urljoin(self.__root, '../queue') url = urllib.parse.urljoin(self.__root, '../queue')
if url and not url.endswith('/'): if url and not url.endswith('/'):
url += '/' url += '/'
raise Raiser(url) raise Raiser(url)
@ -899,7 +850,7 @@ class QueuePage:
searchList=[info], compilerSettings=CHEETAH_DIRECTIVES) searchList=[info], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def delete(self, **kwargs): def delete(self, **kwargs):
uid = kwargs.get('uid') uid = kwargs.get('uid')
del_files = int_conv(kwargs.get('del_files')) del_files = int_conv(kwargs.get('del_files'))
@ -907,12 +858,12 @@ class QueuePage:
NzbQueue.do.remove(uid, add_to_history=False, delete_all_data=del_files) NzbQueue.do.remove(uid, add_to_history=False, delete_all_data=del_files)
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def purge(self, **kwargs): def purge(self, **kwargs):
NzbQueue.do.remove_all(kwargs.get('search')) NzbQueue.do.remove_all(kwargs.get('search'))
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def change_queue_complete_action(self, **kwargs): def change_queue_complete_action(self, **kwargs):
""" Action or script to be performed once the queue has been completed """ Action or script to be performed once the queue has been completed
Scripts are prefixed with 'script_' Scripts are prefixed with 'script_'
@ -921,7 +872,7 @@ class QueuePage:
sabnzbd.change_queue_complete_action(action) sabnzbd.change_queue_complete_action(action)
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def switch(self, **kwargs): def switch(self, **kwargs):
uid1 = kwargs.get('uid1') uid1 = kwargs.get('uid1')
uid2 = kwargs.get('uid2') uid2 = kwargs.get('uid2')
@ -929,7 +880,7 @@ class QueuePage:
NzbQueue.do.switch(uid1, uid2) NzbQueue.do.switch(uid1, uid2)
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def change_opts(self, **kwargs): def change_opts(self, **kwargs):
nzo_id = kwargs.get('nzo_id') nzo_id = kwargs.get('nzo_id')
pp = kwargs.get('pp', '') pp = kwargs.get('pp', '')
@ -937,7 +888,7 @@ class QueuePage:
NzbQueue.do.change_opts(nzo_id, int(pp)) NzbQueue.do.change_opts(nzo_id, int(pp))
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def change_script(self, **kwargs): def change_script(self, **kwargs):
nzo_id = kwargs.get('nzo_id') nzo_id = kwargs.get('nzo_id')
script = kwargs.get('script', '') script = kwargs.get('script', '')
@ -947,7 +898,7 @@ class QueuePage:
NzbQueue.do.change_script(nzo_id, script) NzbQueue.do.change_script(nzo_id, script)
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def change_cat(self, **kwargs): def change_cat(self, **kwargs):
nzo_id = kwargs.get('nzo_id') nzo_id = kwargs.get('nzo_id')
cat = kwargs.get('cat', '') cat = kwargs.get('cat', '')
@ -958,51 +909,51 @@ class QueuePage:
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def shutdown(self, **kwargs): def shutdown(self, **kwargs):
sabnzbd.shutdown_program() sabnzbd.shutdown_program()
return T('SABnzbd shutdown finished') return T('SABnzbd shutdown finished')
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def pause(self, **kwargs): def pause(self, **kwargs):
scheduler.plan_resume(0) scheduler.plan_resume(0)
Downloader.do.pause() Downloader.do.pause()
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def resume(self, **kwargs): def resume(self, **kwargs):
scheduler.plan_resume(0) scheduler.plan_resume(0)
sabnzbd.unpause_all() sabnzbd.unpause_all()
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def pause_nzo(self, **kwargs): def pause_nzo(self, **kwargs):
uid = kwargs.get('uid', '') uid = kwargs.get('uid', '')
NzbQueue.do.pause_multiple_nzo(uid.split(',')) NzbQueue.do.pause_multiple_nzo(uid.split(','))
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def resume_nzo(self, **kwargs): def resume_nzo(self, **kwargs):
uid = kwargs.get('uid', '') uid = kwargs.get('uid', '')
NzbQueue.do.resume_multiple_nzo(uid.split(',')) NzbQueue.do.resume_multiple_nzo(uid.split(','))
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def set_priority(self, **kwargs): def set_priority(self, **kwargs):
NzbQueue.do.set_priority(kwargs.get('nzo_id'), kwargs.get('priority')) NzbQueue.do.set_priority(kwargs.get('nzo_id'), kwargs.get('priority'))
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def sort_by_avg_age(self, **kwargs): def sort_by_avg_age(self, **kwargs):
NzbQueue.do.sort_queue('avg_age', kwargs.get('dir')) NzbQueue.do.sort_queue('avg_age', kwargs.get('dir'))
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def sort_by_name(self, **kwargs): def sort_by_name(self, **kwargs):
NzbQueue.do.sort_queue('name', kwargs.get('dir')) NzbQueue.do.sort_queue('name', kwargs.get('dir'))
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def sort_by_size(self, **kwargs): def sort_by_size(self, **kwargs):
NzbQueue.do.sort_queue('size', kwargs.get('dir')) NzbQueue.do.sort_queue('size', kwargs.get('dir'))
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@ -1054,13 +1005,13 @@ class HistoryPage:
searchList=[history], compilerSettings=CHEETAH_DIRECTIVES) searchList=[history], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def purge(self, **kwargs): def purge(self, **kwargs):
history_db = sabnzbd.get_db_connection() history_db = sabnzbd.get_db_connection()
history_db.remove_history() history_db.remove_history()
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def delete(self, **kwargs): def delete(self, **kwargs):
job = kwargs.get('job') job = kwargs.get('job')
del_files = int_conv(kwargs.get('del_files')) del_files = int_conv(kwargs.get('del_files'))
@ -1070,44 +1021,11 @@ class HistoryPage:
del_hist_job(job, del_files=del_files) del_hist_job(job, del_files=del_files)
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def retry_pp(self, **kwargs): def retry_pp(self, **kwargs):
retry_job(kwargs.get('job'), kwargs.get('nzbfile'), kwargs.get('password')) retry_job(kwargs.get('job'), kwargs.get('nzbfile'), kwargs.get('password'))
raise queueRaiser(self.__root, kwargs) raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True)
def retry_all(self, **kwargs):
retry_all_jobs()
raise queueRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True)
def reset(self, **kwargs):
# sabnzbd.reset_byte_counter()
raise queueRaiser(self.__root, kwargs)
@secured_expose
def scriptlog(self, **kwargs):
""" Duplicate of scriptlog of History, needed for some skins """
# No session key check, due to fixed URLs
name = kwargs.get('name')
if name:
history_db = sabnzbd.get_db_connection()
return ShowString(history_db.get_name(name), history_db.get_script_log(name))
else:
raise Raiser(self.__root)
@secured_expose(check_session_key=True)
def retry(self, **kwargs):
job = kwargs.get('job', '')
url = kwargs.get('url', '').strip()
pp = kwargs.get('pp')
cat = kwargs.get('cat')
script = kwargs.get('script')
if url:
sabnzbd.add_url(url, pp, script, cat, nzbname=kwargs.get('nzbname'))
del_hist_job(job, del_files=True)
raise Raiser(self.__root)
############################################################################## ##############################################################################
class ConfigPage: class ConfigPage:
@ -1151,14 +1069,14 @@ class ConfigPage:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def restart(self, **kwargs): def restart(self, **kwargs):
logging.info('Restart requested by interface') logging.info('Restart requested by interface')
# Do the shutdown async to still send goodbye to browser # Do the shutdown async to still send goodbye to browser
Thread(target=sabnzbd.trigger_restart, kwargs={'timeout': 1}).start() Thread(target=sabnzbd.trigger_restart, kwargs={'timeout': 1}).start()
return T('&nbsp<br />SABnzbd shutdown finished.<br />Wait for about 5 second and then click the button below.<br /><br /><strong><a href="..">Refresh</a></strong><br />') return T('&nbsp<br />SABnzbd shutdown finished.<br />Wait for about 5 second and then click the button below.<br /><br /><strong><a href="..">Refresh</a></strong><br />')
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def repair(self, **kwargs): def repair(self, **kwargs):
logging.info('Queue repair requested by interface') logging.info('Queue repair requested by interface')
sabnzbd.request_repair() sabnzbd.request_repair()
@ -1191,7 +1109,7 @@ class ConfigFolders:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def saveDirectories(self, **kwargs): def saveDirectories(self, **kwargs):
for kw in LIST_DIRPAGE: for kw in LIST_DIRPAGE:
value = kwargs.get(kw) value = kwargs.get(kw)
@ -1257,7 +1175,7 @@ class ConfigSwitches:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def saveSwitches(self, **kwargs): def saveSwitches(self, **kwargs):
for kw in SWITCH_LIST: for kw in SWITCH_LIST:
item = config.get_config('misc', kw) item = config.get_config('misc', kw)
@ -1311,7 +1229,7 @@ class ConfigSpecial:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def saveSpecial(self, **kwargs): def saveSpecial(self, **kwargs):
for kw in SPECIAL_BOOL_LIST + SPECIAL_VALUE_LIST + SPECIAL_LIST_LIST: for kw in SPECIAL_BOOL_LIST + SPECIAL_VALUE_LIST + SPECIAL_LIST_LIST:
item = config.get_config('misc', kw) item = config.get_config('misc', kw)
@ -1407,7 +1325,7 @@ class ConfigGeneral:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def saveGeneral(self, **kwargs): def saveGeneral(self, **kwargs):
# Handle general options # Handle general options
for kw in GENERAL_LIST: for kw in GENERAL_LIST:
@ -1486,33 +1404,33 @@ class ConfigServer:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def addServer(self, **kwargs): def addServer(self, **kwargs):
return handle_server(kwargs, self.__root, True) return handle_server(kwargs, self.__root, True)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def saveServer(self, **kwargs): def saveServer(self, **kwargs):
return handle_server(kwargs, self.__root) return handle_server(kwargs, self.__root)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def testServer(self, **kwargs): def testServer(self, **kwargs):
return handle_server_test(kwargs, self.__root) return handle_server_test(kwargs, self.__root)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def delServer(self, **kwargs): def delServer(self, **kwargs):
kwargs['section'] = 'servers' kwargs['section'] = 'servers'
kwargs['keyword'] = kwargs.get('server') kwargs['keyword'] = kwargs.get('server')
del_from_section(kwargs) del_from_section(kwargs)
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def clrServer(self, **kwargs): def clrServer(self, **kwargs):
server = kwargs.get('server') server = kwargs.get('server')
if server: if server:
BPSMeter.do.clear_server(server) BPSMeter.do.clear_server(server)
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def toggleServer(self, **kwargs): def toggleServer(self, **kwargs):
server = kwargs.get('server') server = kwargs.get('server')
if server: if server:
@ -1693,7 +1611,7 @@ class ConfigRss:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def save_rss_rate(self, **kwargs): def save_rss_rate(self, **kwargs):
""" Save changed RSS automatic readout rate """ """ Save changed RSS automatic readout rate """
cfg.rss_rate.set(kwargs.get('rss_rate')) cfg.rss_rate.set(kwargs.get('rss_rate'))
@ -1701,7 +1619,7 @@ class ConfigRss:
scheduler.restart() scheduler.restart()
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def upd_rss_feed(self, **kwargs): def upd_rss_feed(self, **kwargs):
""" Update Feed level attributes, """ Update Feed level attributes,
legacy version: ignores 'enable' parameter legacy version: ignores 'enable' parameter
@ -1722,7 +1640,7 @@ class ConfigRss:
self.__show_eval_button = True self.__show_eval_button = True
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def save_rss_feed(self, **kwargs): def save_rss_feed(self, **kwargs):
""" Update Feed level attributes """ """ Update Feed level attributes """
try: try:
@ -1739,7 +1657,7 @@ class ConfigRss:
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def toggle_rss_feed(self, **kwargs): def toggle_rss_feed(self, **kwargs):
""" Toggle automatic read-out flag of Feed """ """ Toggle automatic read-out flag of Feed """
try: try:
@ -1754,7 +1672,7 @@ class ConfigRss:
else: else:
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def add_rss_feed(self, **kwargs): def add_rss_feed(self, **kwargs):
""" Add one new RSS feed definition """ """ Add one new RSS feed definition """
feed = Strip(kwargs.get('feed')).strip('[]') feed = Strip(kwargs.get('feed')).strip('[]')
@ -1783,7 +1701,7 @@ class ConfigRss:
else: else:
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def upd_rss_filter(self, **kwargs): def upd_rss_filter(self, **kwargs):
""" Wrapper, so we can call from api.py """ """ Wrapper, so we can call from api.py """
self.internal_upd_rss_filter(**kwargs) self.internal_upd_rss_filter(**kwargs)
@ -1819,7 +1737,7 @@ class ConfigRss:
self.__show_eval_button = True self.__show_eval_button = True
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def del_rss_feed(self, *args, **kwargs): def del_rss_feed(self, *args, **kwargs):
""" Remove complete RSS feed """ """ Remove complete RSS feed """
kwargs['section'] = 'rss' kwargs['section'] = 'rss'
@ -1828,7 +1746,7 @@ class ConfigRss:
sabnzbd.rss.clear_feed(kwargs.get('feed')) sabnzbd.rss.clear_feed(kwargs.get('feed'))
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def del_rss_filter(self, **kwargs): def del_rss_filter(self, **kwargs):
""" Wrapper, so we can call from api.py """ """ Wrapper, so we can call from api.py """
self.internal_del_rss_filter(**kwargs) self.internal_del_rss_filter(**kwargs)
@ -1846,7 +1764,7 @@ class ConfigRss:
self.__show_eval_button = True self.__show_eval_button = True
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def download_rss_feed(self, *args, **kwargs): def download_rss_feed(self, *args, **kwargs):
""" Force download of all matching jobs in a feed """ """ Force download of all matching jobs in a feed """
if 'feed' in kwargs: if 'feed' in kwargs:
@ -1858,14 +1776,14 @@ class ConfigRss:
self.__evaluate = True self.__evaluate = True
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def clean_rss_jobs(self, *args, **kwargs): def clean_rss_jobs(self, *args, **kwargs):
""" Remove processed RSS jobs from UI """ """ Remove processed RSS jobs from UI """
sabnzbd.rss.clear_downloaded(kwargs['feed']) sabnzbd.rss.clear_downloaded(kwargs['feed'])
self.__evaluate = True self.__evaluate = True
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def test_rss_feed(self, *args, **kwargs): def test_rss_feed(self, *args, **kwargs):
""" Read the feed content again and show results """ """ Read the feed content again and show results """
if 'feed' in kwargs: if 'feed' in kwargs:
@ -1878,7 +1796,7 @@ class ConfigRss:
self.__show_eval_button = False self.__show_eval_button = False
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def eval_rss_feed(self, *args, **kwargs): def eval_rss_feed(self, *args, **kwargs):
""" Re-apply the filters to the feed """ """ Re-apply the filters to the feed """
if 'feed' in kwargs: if 'feed' in kwargs:
@ -1890,7 +1808,7 @@ class ConfigRss:
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def download(self, **kwargs): def download(self, **kwargs):
""" Download NZB from provider (Download button) """ """ Download NZB from provider (Download button) """
feed = kwargs.get('feed') feed = kwargs.get('feed')
@ -1909,7 +1827,7 @@ class ConfigRss:
sabnzbd.rss.flag_downloaded(feed, url) sabnzbd.rss.flag_downloaded(feed, url)
raise rssRaiser(self.__root, kwargs) raise rssRaiser(self.__root, kwargs)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def rss_now(self, *args, **kwargs): def rss_now(self, *args, **kwargs):
""" Run an automatic RSS run now """ """ Run an automatic RSS run now """
scheduler.force_rss() scheduler.force_rss()
@ -2037,7 +1955,7 @@ class ConfigScheduling:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def addSchedule(self, **kwargs): def addSchedule(self, **kwargs):
servers = config.get_servers() servers = config.get_servers()
minute = kwargs.get('minute') minute = kwargs.get('minute')
@ -2085,7 +2003,7 @@ class ConfigScheduling:
scheduler.restart(force=True) scheduler.restart(force=True)
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def delSchedule(self, **kwargs): def delSchedule(self, **kwargs):
schedules = cfg.schedules() schedules = cfg.schedules()
line = kwargs.get('line') line = kwargs.get('line')
@ -2096,7 +2014,7 @@ class ConfigScheduling:
scheduler.restart(force=True) scheduler.restart(force=True)
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def toggleSchedule(self, **kwargs): def toggleSchedule(self, **kwargs):
schedules = cfg.schedules() schedules = cfg.schedules()
line = kwargs.get('line') line = kwargs.get('line')
@ -2144,14 +2062,14 @@ class ConfigCats:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def delete(self, **kwargs): def delete(self, **kwargs):
kwargs['section'] = 'categories' kwargs['section'] = 'categories'
kwargs['keyword'] = kwargs.get('name') kwargs['keyword'] = kwargs.get('name')
del_from_section(kwargs) del_from_section(kwargs)
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def save(self, **kwargs): def save(self, **kwargs):
name = kwargs.get('name', '*') name = kwargs.get('name', '*')
if name == '*': if name == '*':
@ -2198,7 +2116,7 @@ class ConfigSorting:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def saveSorting(self, **kwargs): def saveSorting(self, **kwargs):
try: try:
kwargs['movie_categories'] = kwargs['movie_cat'] kwargs['movie_categories'] = kwargs['movie_cat']
@ -2244,22 +2162,22 @@ class Status:
searchList=[header], compilerSettings=CHEETAH_DIRECTIVES) searchList=[header], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def reset_quota(self, **kwargs): def reset_quota(self, **kwargs):
BPSMeter.do.reset_quota(force=True) BPSMeter.do.reset_quota(force=True)
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def disconnect(self, **kwargs): def disconnect(self, **kwargs):
Downloader.do.disconnect() Downloader.do.disconnect()
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def refresh_conn(self, **kwargs): def refresh_conn(self, **kwargs):
# No real action, just reload the page # No real action, just reload the page
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def showlog(self, **kwargs): def showlog(self, **kwargs):
try: try:
sabnzbd.LOGHANDLER.flush() sabnzbd.LOGHANDLER.flush()
@ -2298,46 +2216,46 @@ class Status:
cherrypy.response.headers['Content-Disposition'] = 'attachment;filename="sabnzbd.log"' cherrypy.response.headers['Content-Disposition'] = 'attachment;filename="sabnzbd.log"'
return log_data return log_data
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def clearwarnings(self, **kwargs): def clearwarnings(self, **kwargs):
sabnzbd.GUIHANDLER.clear() sabnzbd.GUIHANDLER.clear()
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def change_loglevel(self, **kwargs): def change_loglevel(self, **kwargs):
cfg.log_level.set(kwargs.get('loglevel')) cfg.log_level.set(kwargs.get('loglevel'))
config.save_config() config.save_config()
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def unblock_server(self, **kwargs): def unblock_server(self, **kwargs):
Downloader.do.unblock(kwargs.get('server')) Downloader.do.unblock(kwargs.get('server'))
# Short sleep so that UI shows new server status # Short sleep so that UI shows new server status
time.sleep(1.0) time.sleep(1.0)
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def delete(self, **kwargs): def delete(self, **kwargs):
orphan_delete(kwargs) orphan_delete(kwargs)
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def delete_all(self, **kwargs): def delete_all(self, **kwargs):
orphan_delete_all() orphan_delete_all()
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def add(self, **kwargs): def add(self, **kwargs):
orphan_add(kwargs) orphan_add(kwargs)
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def add_all(self, **kwargs): def add_all(self, **kwargs):
orphan_add_all() orphan_add_all()
raise Raiser(self.__root) raise Raiser(self.__root)
@secured_expose(check_session_key=True) @secured_expose(check_api_key=True)
def dashrefresh(self, **kwargs): def dashrefresh(self, **kwargs):
# This function is run when Refresh button on Dashboard is clicked # This function is run when Refresh button on Dashboard is clicked
# Put the time consuming dashboard functions here; they only get executed when the user clicks the Refresh button # Put the time consuming dashboard functions here; they only get executed when the user clicks the Refresh button
@ -2577,7 +2495,7 @@ class ConfigNotify:
searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES) searchList=[conf], compilerSettings=CHEETAH_DIRECTIVES)
return template.respond() return template.respond()
@secured_expose(check_session_key=True, check_configlock=True) @secured_expose(check_api_key=True, check_configlock=True)
def saveEmail(self, **kwargs): def saveEmail(self, **kwargs):
ajax = kwargs.get('ajax') ajax = kwargs.get('ajax')

2
tests/testhelper.py

@ -102,7 +102,7 @@ def set_platform(platform):
def get_url_result(url="", host=SAB_HOST, port=SAB_PORT): def get_url_result(url="", host=SAB_HOST, port=SAB_PORT):
""" Do basic request to web page """ """ Do basic request to web page """
arguments = {"session": "apikey"} arguments = {"apikey": "apikey"}
return requests.get("http://%s:%s/%s/" % (host, port, url), params=arguments).text return requests.get("http://%s:%s/%s/" % (host, port, url), params=arguments).text

Loading…
Cancel
Save