New: Added CAPTCHA support to Rarbg.
This commit is contained in:
parent
e9eab0ae48
commit
73fb216e6f
|
@ -81,7 +81,7 @@ namespace NzbDrone.Api.Extensions.Pipelines
|
|||
{
|
||||
if (request.Url.Query.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return string.Concat(request.Url.Path, request.Url.Query);
|
||||
return string.Concat(request.Url.Path, "?", request.Url.Query);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace NzbDrone.Api
|
|||
|
||||
Get["schema"] = x => GetTemplates();
|
||||
Post["test"] = x => Test(ReadResourceFromRequest(true));
|
||||
Post["connectData/{stage}"] = x => ConnectData(x.stage, ReadResourceFromRequest(true));
|
||||
Post["action/{action}"] = x => RequestAction(x.action, ReadResourceFromRequest(true));
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetProviderById;
|
||||
|
@ -183,13 +183,13 @@ namespace NzbDrone.Api
|
|||
}
|
||||
|
||||
|
||||
private Response ConnectData(string stage, TProviderResource providerResource)
|
||||
private Response RequestAction(string action, TProviderResource providerResource)
|
||||
{
|
||||
var providerDefinition = GetDefinition(providerResource, true, false);
|
||||
|
||||
if (!providerDefinition.Enable) return "{}";
|
||||
var query = ((IDictionary<string, object>)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString());
|
||||
|
||||
object data = _providerFactory.ConnectData(providerDefinition, stage, (IDictionary<string, object>) Request.Query.ToDictionary());
|
||||
var data = _providerFactory.RequestAction(providerDefinition, action, query);
|
||||
Response resp = JsonConvert.SerializeObject(data);
|
||||
resp.ContentType = "application/json";
|
||||
return resp;
|
||||
|
|
|
@ -104,7 +104,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||
default:
|
||||
throw new NotSupportedException(string.Format("HttpCurl method {0} not supported", request.Method));
|
||||
}
|
||||
curlEasy.UserAgent = UserAgentBuilder.UserAgent;
|
||||
curlEasy.UserAgent = request.UseSimplifiedUserAgent ? UserAgentBuilder.UserAgentSimplified : UserAgentBuilder.UserAgent; ;
|
||||
curlEasy.FollowLocation = request.AllowAutoRedirect;
|
||||
|
||||
if (request.RequestTimeout != TimeSpan.Zero)
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
||||
|
||||
webRequest.Method = request.Method.ToString();
|
||||
webRequest.UserAgent = UserAgentBuilder.UserAgent;
|
||||
webRequest.UserAgent = request.UseSimplifiedUserAgent ? UserAgentBuilder.UserAgentSimplified : UserAgentBuilder.UserAgent;
|
||||
webRequest.KeepAlive = request.ConnectionKeepAlive;
|
||||
webRequest.AllowAutoRedirect = request.AllowAutoRedirect;
|
||||
webRequest.CookieContainer = cookies;
|
||||
|
|
|
@ -34,6 +34,7 @@ namespace NzbDrone.Common.Http
|
|||
public byte[] ContentData { get; set; }
|
||||
public string ContentSummary { get; set; }
|
||||
public bool SuppressHttpError { get; set; }
|
||||
public bool UseSimplifiedUserAgent { get; set; }
|
||||
public bool AllowAutoRedirect { get; set; }
|
||||
public bool ConnectionKeepAlive { get; set; }
|
||||
public bool LogResponseContent { get; set; }
|
||||
|
|
|
@ -19,6 +19,7 @@ namespace NzbDrone.Common.Http
|
|||
public Dictionary<string, string> Segments { get; private set; }
|
||||
public HttpHeader Headers { get; private set; }
|
||||
public bool SuppressHttpError { get; set; }
|
||||
public bool UseSimplifiedUserAgent { get; set; }
|
||||
public bool AllowAutoRedirect { get; set; }
|
||||
public bool ConnectionKeepAlive { get; set; }
|
||||
public bool LogResponseContent { get; set; }
|
||||
|
@ -99,6 +100,7 @@ namespace NzbDrone.Common.Http
|
|||
{
|
||||
request.Method = Method;
|
||||
request.SuppressHttpError = SuppressHttpError;
|
||||
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
|
||||
request.AllowAutoRedirect = AllowAutoRedirect;
|
||||
request.ConnectionKeepAlive = ConnectionKeepAlive;
|
||||
request.LogResponseContent = LogResponseContent;
|
||||
|
|
|
@ -6,12 +6,16 @@ namespace NzbDrone.Common.Http
|
|||
public static class UserAgentBuilder
|
||||
{
|
||||
public static string UserAgent { get; private set; }
|
||||
public static string UserAgentSimplified { get; private set; }
|
||||
|
||||
static UserAgentBuilder()
|
||||
{
|
||||
UserAgent = string.Format("Sonarr/{0} ({1} {2}) ",
|
||||
UserAgent = string.Format("Sonarr/{0} ({1} {2})",
|
||||
BuildInfo.Version,
|
||||
OsInfo.Os, OsInfo.Version.ToString(2));
|
||||
|
||||
UserAgentSimplified = string.Format("Sonarr/{0}",
|
||||
BuildInfo.Version.ToString(2));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ namespace NzbDrone.Core.Annotations
|
|||
Hidden,
|
||||
Tag,
|
||||
Action,
|
||||
Url
|
||||
Url,
|
||||
Captcha
|
||||
}
|
||||
}
|
|
@ -50,7 +50,7 @@ namespace NzbDrone.Core.Download
|
|||
|
||||
public ProviderDefinition Definition { get; set; }
|
||||
|
||||
public object ConnectData(string stage, IDictionary<string, object> query) { return null; }
|
||||
public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }
|
||||
|
||||
protected TSettings Settings
|
||||
{
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
using NzbDrone.Common.Exceptions;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.Http.CloudFlare
|
||||
{
|
||||
public class CloudFlareCaptchaException : NzbDroneException
|
||||
{
|
||||
public HttpResponse Response { get; set; }
|
||||
|
||||
public CloudFlareCaptchaRequest CaptchaRequest { get; set; }
|
||||
|
||||
public CloudFlareCaptchaException(HttpResponse response, CloudFlareCaptchaRequest captchaRequest)
|
||||
: base("Unable to access {0}, blocked by CloudFlare CAPTCHA. Likely due to shared-IP VPN.", response.Request.Url.Host)
|
||||
{
|
||||
Response = response;
|
||||
CaptchaRequest = captchaRequest;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.Http.CloudFlare
|
||||
{
|
||||
public class CloudFlareCaptchaRequest
|
||||
{
|
||||
public string Host { get; set; }
|
||||
public string SiteKey { get; set; }
|
||||
|
||||
public string Ray { get; set; }
|
||||
public string SecretToken { get; set; }
|
||||
|
||||
public HttpUri ResponseUrl { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.Http.CloudFlare
|
||||
{
|
||||
public class CloudFlareHttpInterceptor : IHttpRequestInterceptor
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
private const string _cloudFlareChallengeScript = "cdn-cgi/scripts/cf.challenge.js";
|
||||
private static readonly Regex _cloudFlareRegex = new Regex(@"data-ray=""(?<Ray>[\w-_]+)"".*?data-sitekey=""(?<SiteKey>[\w-_]+)"".*?data-stoken=""(?<SecretToken>[\w-_]+)""", RegexOptions.Compiled);
|
||||
|
||||
public CloudFlareHttpInterceptor(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public HttpRequest PreRequest(HttpRequest request)
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
public HttpResponse PostResponse(HttpResponse response)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.Forbidden && response.Content.Contains(_cloudFlareChallengeScript))
|
||||
{
|
||||
_logger.Error("CloudFlare CAPTCHA block on {0}", response.Request.Url);
|
||||
throw new CloudFlareCaptchaException(response, CreateCaptchaRequest(response));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private CloudFlareCaptchaRequest CreateCaptchaRequest(HttpResponse response)
|
||||
{
|
||||
var match = _cloudFlareRegex.Match(response.Content);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CloudFlareCaptchaRequest
|
||||
{
|
||||
Host = response.Request.Url.Host,
|
||||
SiteKey = match.Groups["SiteKey"].Value,
|
||||
Ray = match.Groups["Ray"].Value,
|
||||
SecretToken = match.Groups["SecretToken"].Value,
|
||||
ResponseUrl = response.Request.Url + new HttpUri("/cdn-cgi/l/chk_captcha")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ using NzbDrone.Common.Extensions;
|
|||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Http.CloudFlare;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
@ -21,7 +22,7 @@ namespace NzbDrone.Core.Indexers
|
|||
{
|
||||
protected const int MaxNumResultsPerQuery = 1000;
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
protected readonly IHttpClient _httpClient;
|
||||
|
||||
public override bool SupportsRss { get { return true; } }
|
||||
public override bool SupportsSearch { get { return true; } }
|
||||
|
@ -313,6 +314,10 @@ namespace NzbDrone.Core.Indexers
|
|||
{
|
||||
_logger.Warn("Request limit reached");
|
||||
}
|
||||
catch (CloudFlareCaptchaException)
|
||||
{
|
||||
return new ValidationFailure("CaptchaToken", "Site protected by CloudFlare CAPTCHA. Valid CAPTCHA token required.");
|
||||
}
|
||||
catch (UnsupportedFeedException ex)
|
||||
{
|
||||
_logger.Warn(ex, "Indexer feed is not supported");
|
||||
|
|
|
@ -65,7 +65,8 @@ namespace NzbDrone.Core.Indexers
|
|||
}
|
||||
|
||||
public virtual ProviderDefinition Definition { get; set; }
|
||||
public object ConnectData(string stage, IDictionary<string, object> query) { return null; }
|
||||
|
||||
public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }
|
||||
|
||||
protected TSettings Settings
|
||||
{
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Http.CloudFlare;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Rarbg
|
||||
{
|
||||
|
@ -30,5 +35,78 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
|||
{
|
||||
return new RarbgParser();
|
||||
}
|
||||
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (action == "checkCaptcha")
|
||||
{
|
||||
Settings.Validate().Filter("BaseUrl").ThrowOnError();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestBuilder(Settings.BaseUrl.Trim('/'))
|
||||
.Resource("/pubapi_v2.php?get_token=get_token")
|
||||
.Accept(HttpAccept.Json)
|
||||
.Build();
|
||||
|
||||
_httpClient.Get(request);
|
||||
}
|
||||
catch (CloudFlareCaptchaException ex)
|
||||
{
|
||||
return new
|
||||
{
|
||||
captchaRequest = new
|
||||
{
|
||||
host = ex.CaptchaRequest.Host,
|
||||
ray = ex.CaptchaRequest.Ray,
|
||||
siteKey = ex.CaptchaRequest.SiteKey,
|
||||
secretToken = ex.CaptchaRequest.SecretToken,
|
||||
responseUrl = ex.CaptchaRequest.ResponseUrl.FullUri,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
captchaToken = ""
|
||||
};
|
||||
}
|
||||
else if (action == "getCaptchaCookie")
|
||||
{
|
||||
if (query["responseUrl"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam responseUrl invalid.");
|
||||
}
|
||||
|
||||
if (query["ray"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam ray invalid.");
|
||||
}
|
||||
|
||||
if (query["captchaResponse"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam captchaResponse invalid.");
|
||||
}
|
||||
|
||||
var request = new HttpRequestBuilder(query["responseUrl"])
|
||||
.AddQueryParam("id", query["ray"])
|
||||
.AddQueryParam("g-recaptcha-response", query["captchaResponse"])
|
||||
.Build();
|
||||
|
||||
request.UseSimplifiedUserAgent = true;
|
||||
request.AllowAutoRedirect = false;
|
||||
|
||||
var response = _httpClient.Get(request);
|
||||
|
||||
var cfClearanceCookie = response.GetCookies()["cf_clearance"];
|
||||
|
||||
return new
|
||||
{
|
||||
captchaToken = cfClearanceCookie
|
||||
};
|
||||
}
|
||||
|
||||
return new { };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -79,6 +79,12 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
|||
.Resource("/pubapi_v2.php")
|
||||
.Accept(HttpAccept.Json);
|
||||
|
||||
if (Settings.CaptchaToken.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
requestBuilder.UseSimplifiedUserAgent = true;
|
||||
requestBuilder.SetCookie("cf_clearance", Settings.CaptchaToken);
|
||||
}
|
||||
|
||||
requestBuilder.AddQueryParam("mode", mode);
|
||||
|
||||
if (tvdbId.HasValue)
|
||||
|
|
|
@ -30,6 +30,9 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
|||
[FieldDefinition(1, Type = FieldType.Checkbox, Label = "Ranked Only", HelpText = "Only include ranked results.")]
|
||||
public bool RankedOnly { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Captcha, Label = "CAPTCHA Token", HelpText = "CAPTCHA Clearance token used to handle CloudFlare Anti-DDOS measures on shared-ip VPNs.")]
|
||||
public string CaptchaToken { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Rarbg
|
||||
|
@ -28,9 +29,17 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
|||
{
|
||||
return _tokenCache.Get(settings.BaseUrl, () =>
|
||||
{
|
||||
var url = settings.BaseUrl.Trim('/') + "/pubapi_v2.php?get_token=get_token";
|
||||
var requestBuilder = new HttpRequestBuilder(settings.BaseUrl.Trim('/'))
|
||||
.Resource("/pubapi_v2.php?get_token=get_token")
|
||||
.Accept(HttpAccept.Json);
|
||||
|
||||
var response = _httpClient.Get<JObject>(new HttpRequest(url, HttpAccept.Json));
|
||||
if (settings.CaptchaToken.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
requestBuilder.UseSimplifiedUserAgent = true;
|
||||
requestBuilder.SetCookie("cf_clearance", settings.CaptchaToken);
|
||||
}
|
||||
|
||||
var response = _httpClient.Get<JObject>(requestBuilder.Build());
|
||||
|
||||
return response.Resource["token"].ToString();
|
||||
}, TimeSpan.FromMinutes(14.0));
|
||||
|
|
|
@ -52,7 +52,7 @@ namespace NzbDrone.Core.Metadata
|
|||
public abstract List<ImageFileResult> SeasonImages(Series series, Season season);
|
||||
public abstract List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile);
|
||||
|
||||
public object ConnectData(string stage, IDictionary<string, object> query) { return null; }
|
||||
public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }
|
||||
|
||||
protected TSettings Settings
|
||||
{
|
||||
|
|
|
@ -61,7 +61,7 @@ namespace NzbDrone.Core.Notifications
|
|||
return GetType().Name;
|
||||
}
|
||||
|
||||
public virtual object ConnectData(string stage, IDictionary<string, object> query) { return null; }
|
||||
public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
namespace NzbDrone.Core.Notifications.Twitter
|
||||
{
|
||||
public class OAuthToken
|
||||
{
|
||||
public string AccessToken { get; set; }
|
||||
public string AccessTokenSecret { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Twitter
|
||||
{
|
||||
|
@ -34,26 +36,45 @@ namespace NzbDrone.Core.Notifications.Twitter
|
|||
{
|
||||
}
|
||||
|
||||
public override object ConnectData(string stage, IDictionary<string, object> query)
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (stage == "step1")
|
||||
if (action == "startOAuth")
|
||||
{
|
||||
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
|
||||
|
||||
if (query["callbackUrl"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam callbackUrl invalid.");
|
||||
}
|
||||
|
||||
var oauthRedirectUrl = _twitterService.GetOAuthRedirect(Settings.ConsumerKey, Settings.ConsumerSecret, query["callbackUrl"]);
|
||||
return new
|
||||
{
|
||||
nextStep = "step2",
|
||||
action = "openWindow",
|
||||
url = _twitterService.GetOAuthRedirect(query["consumerKey"].ToString(), query["consumerSecret"].ToString(), query["callbackUrl"].ToString())
|
||||
oauthUrl = oauthRedirectUrl
|
||||
};
|
||||
}
|
||||
else if (stage == "step2")
|
||||
else if (action == "getOAuthToken")
|
||||
{
|
||||
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
|
||||
|
||||
if (query["oauth_token"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam oauth_token invalid.");
|
||||
}
|
||||
|
||||
if (query["oauth_verifier"].IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new BadRequestException("QueryParam oauth_verifier invalid.");
|
||||
}
|
||||
|
||||
var oauthToken = _twitterService.GetOAuthToken(Settings.ConsumerKey, Settings.ConsumerSecret, query["oauth_token"], query["oauth_verifier"]);
|
||||
return new
|
||||
{
|
||||
action = "updateFields",
|
||||
fields = _twitterService.GetOAuthToken(query["consumerKey"].ToString(), query["consumerSecret"].ToString(), query["oauth_token"].ToString(), query["oauth_verifier"].ToString())
|
||||
accessToken = oauthToken.AccessToken,
|
||||
accessTokenSecret = oauthToken.AccessTokenSecret
|
||||
};
|
||||
}
|
||||
return new {};
|
||||
return new { };
|
||||
}
|
||||
|
||||
public override string Name
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace NzbDrone.Core.Notifications.Twitter
|
|||
void SendNotification(string message, TwitterSettings settings);
|
||||
ValidationFailure Test(TwitterSettings settings);
|
||||
string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl);
|
||||
object GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier);
|
||||
OAuthToken GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier);
|
||||
}
|
||||
|
||||
public class TwitterService : ITwitterService
|
||||
|
@ -43,14 +43,14 @@ namespace NzbDrone.Core.Notifications.Twitter
|
|||
return HttpUtility.ParseQueryString(response.Content);
|
||||
}
|
||||
|
||||
public object GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier)
|
||||
public OAuthToken GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier)
|
||||
{
|
||||
// Creating a new instance with a helper method
|
||||
var oAuthRequest = OAuthRequest.ForAccessToken(consumerKey, consumerSecret, oauthToken, "", oauthVerifier);
|
||||
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/access_token";
|
||||
var qscoll = OAuthQuery(oAuthRequest);
|
||||
|
||||
return new
|
||||
return new OAuthToken
|
||||
{
|
||||
AccessToken = qscoll["oauth_token"],
|
||||
AccessTokenSecret = qscoll["oauth_token_secret"]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
@ -19,6 +20,10 @@ namespace NzbDrone.Core.Notifications.Twitter
|
|||
RuleFor(c => c.DirectMessage).Equal(true)
|
||||
.WithMessage("Using Direct Messaging is recommended, or use a private account.")
|
||||
.AsWarning();
|
||||
|
||||
RuleFor(c => c.AuthorizeNotification).Empty()
|
||||
.When(c => c.AccessToken.IsNullOrWhiteSpace() || c.AccessTokenSecret.IsNullOrWhiteSpace())
|
||||
.WithMessage("Authenticate app.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -527,6 +527,9 @@
|
|||
<Compile Include="Housekeeping\HousekeepingCommand.cs" />
|
||||
<Compile Include="Housekeeping\HousekeepingService.cs" />
|
||||
<Compile Include="Housekeeping\IHousekeepingTask.cs" />
|
||||
<Compile Include="Http\CloudFlare\CloudFlareCaptchaException.cs" />
|
||||
<Compile Include="Http\CloudFlare\CloudFlareCaptchaRequest.cs" />
|
||||
<Compile Include="Http\CloudFlare\CloudFlareHttpInterceptor.cs" />
|
||||
<Compile Include="Http\HttpProxySettingsProvider.cs" />
|
||||
<Compile Include="Http\TorcacheHttpInterceptor.cs" />
|
||||
<Compile Include="Indexers\BitMeTv\BitMeTv.cs" />
|
||||
|
@ -810,6 +813,7 @@
|
|||
<Compile Include="Notifications\Synology\SynologyIndexer.cs" />
|
||||
<Compile Include="Notifications\Synology\SynologyIndexerProxy.cs" />
|
||||
<Compile Include="Notifications\Synology\SynologyIndexerSettings.cs" />
|
||||
<Compile Include="Notifications\Twitter\OAuthToken.cs" />
|
||||
<Compile Include="Notifications\Twitter\TwitterException.cs" />
|
||||
<Compile Include="Notifications\Webhook\WebhookEpisode.cs" />
|
||||
<Compile Include="Notifications\Webhook\WebhookException.cs" />
|
||||
|
@ -1054,6 +1058,7 @@
|
|||
<Compile Include="Validation\FolderValidator.cs" />
|
||||
<Compile Include="Validation\IpValidation.cs" />
|
||||
<Compile Include="Validation\LanguageValidator.cs" />
|
||||
<Compile Include="Validation\NzbDroneValidationExtensions.cs" />
|
||||
<Compile Include="Validation\NzbDroneValidationFailure.cs" />
|
||||
<Compile Include="Validation\NzbDroneValidationResult.cs" />
|
||||
<Compile Include="Validation\NzbDroneValidationState.cs" />
|
||||
|
|
|
@ -12,6 +12,6 @@ namespace NzbDrone.Core.ThingiProvider
|
|||
IEnumerable<ProviderDefinition> DefaultDefinitions { get; }
|
||||
ProviderDefinition Definition { get; set; }
|
||||
ValidationResult Test();
|
||||
object ConnectData(string stage, IDictionary<string, object> query);
|
||||
object RequestAction(string stage, IDictionary<string, string> query);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,6 @@ namespace NzbDrone.Core.ThingiProvider
|
|||
void SetProviderCharacteristics(TProvider provider, TProviderDefinition definition);
|
||||
TProvider GetInstance(TProviderDefinition definition);
|
||||
ValidationResult Test(TProviderDefinition definition);
|
||||
object ConnectData(TProviderDefinition definition, string stage, IDictionary<string, object> query );
|
||||
object RequestAction(TProviderDefinition definition, string action, IDictionary<string, string> query);
|
||||
}
|
||||
}
|
|
@ -81,9 +81,9 @@ namespace NzbDrone.Core.ThingiProvider
|
|||
return GetInstance(definition).Test();
|
||||
}
|
||||
|
||||
public object ConnectData(TProviderDefinition definition, string stage, IDictionary<string, object> query)
|
||||
public object RequestAction(TProviderDefinition definition, string action, IDictionary<string, string> query)
|
||||
{
|
||||
return GetInstance(definition).ConnectData(stage, query);
|
||||
return GetInstance(definition).RequestAction(action, query);
|
||||
}
|
||||
|
||||
public List<TProvider> GetAvailableProviders()
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
|
||||
namespace NzbDrone.Core.Validation
|
||||
{
|
||||
public static class NzbDroneValidationExtensions
|
||||
{
|
||||
public static NzbDroneValidationResult Filter(this NzbDroneValidationResult result, params string[] fields)
|
||||
{
|
||||
var failures = result.Failures.Where(v => fields.Contains(v.PropertyName)).ToArray();
|
||||
|
||||
return new NzbDroneValidationResult(failures);
|
||||
}
|
||||
|
||||
public static void ThrowOnError(this NzbDroneValidationResult result)
|
||||
{
|
||||
if (!result.IsValid)
|
||||
{
|
||||
throw new ValidationException(result.Errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,6 @@
|
|||
<label class="col-sm-3 control-label"></label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<button class="form-control {{name}}" data-value="{{value}}">{{label}}</button>
|
||||
<button class="form-control {{name}}" validation-name="{{name}}" data-value="{{value}}">{{label}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<div class="form-group {{#if advanced}}advanced-setting{{/if}}">
|
||||
<label class="col-sm-3 control-label">{{label}}</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<div class="input-group">
|
||||
<input type="text" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false" class="form-control x-captcha" readonly />
|
||||
<span class="input-group-btn"><button class="btn btn-primary x-captcha-refresh" title="Refresh CAPTCHA Token"><i class="icon-sonarr-refresh" /></button></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="col-sm-1 help-inline">
|
||||
<i class="icon-sonarr-form-warning" title="Expires periodically and will need to be refreshed. Refreshing the CAPTCHA Token will embed a temporary Google reCaptcha widget on this page."/>
|
||||
</span>
|
||||
</div>
|
|
@ -49,6 +49,10 @@ var _fieldBuilder = function(field) {
|
|||
return _templateRenderer.call(field, 'Form/ActionTemplate');
|
||||
}
|
||||
|
||||
if (field.type === 'captcha') {
|
||||
return _templateRenderer.call(field, 'Form/CaptchaTemplate');
|
||||
}
|
||||
|
||||
return _templateRenderer.call(field, 'Form/TextboxTemplate');
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var vent = require('vent');
|
||||
var Marionette = require('marionette');
|
||||
var DeleteView = require('../Delete/IndexerDeleteView');
|
||||
|
@ -12,7 +14,8 @@ var view = Marionette.ItemView.extend({
|
|||
template : 'Settings/Indexers/Edit/IndexerEditViewTemplate',
|
||||
|
||||
events : {
|
||||
'click .x-back' : '_back'
|
||||
'click .x-back' : '_back',
|
||||
'click .x-captcha-refresh' : '_onRefreshCaptcha'
|
||||
},
|
||||
|
||||
_deleteView : DeleteView,
|
||||
|
@ -38,6 +41,77 @@ var view = Marionette.ItemView.extend({
|
|||
}
|
||||
|
||||
require('../Add/IndexerSchemaModal').open(this.targetCollection);
|
||||
},
|
||||
|
||||
_onRefreshCaptcha : function(event) {
|
||||
var self = this;
|
||||
|
||||
var target = $(event.target).parents('.input-group');
|
||||
|
||||
this.ui.indicator.show();
|
||||
|
||||
this.model.requestAction("checkCaptcha")
|
||||
.then(function(result) {
|
||||
if (!result.captchaRequest) {
|
||||
self.model.setFieldValue('CaptchaToken', '');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return self._showCaptcha(target, result.captchaRequest);
|
||||
})
|
||||
.always(function() {
|
||||
self.ui.indicator.hide();
|
||||
});
|
||||
},
|
||||
|
||||
_showCaptcha : function(target, captchaRequest) {
|
||||
var self = this;
|
||||
|
||||
var widget = $('<div class="g-recaptcha"></div>').insertAfter(target);
|
||||
|
||||
return this._loadRecaptchaWidget(widget[0], captchaRequest.siteKey, captchaRequest.secretToken)
|
||||
.then(function(captchaResponse) {
|
||||
target.parents('.form-group').removeAllErrors();
|
||||
widget.remove();
|
||||
|
||||
var queryParams = {
|
||||
responseUrl : captchaRequest.responseUrl,
|
||||
ray : captchaRequest.ray,
|
||||
captchaResponse: captchaResponse
|
||||
};
|
||||
|
||||
return self.model.requestAction("getCaptchaCookie", queryParams);
|
||||
})
|
||||
.then(function(response) {
|
||||
self.model.setFieldValue('CaptchaToken', response.captchaToken);
|
||||
});
|
||||
},
|
||||
|
||||
_loadRecaptchaWidget : function(widget, sitekey, stoken) {
|
||||
var promise = $.Deferred();
|
||||
|
||||
var renderWidget = function() {
|
||||
window.grecaptcha.render(widget, {
|
||||
'sitekey' : sitekey,
|
||||
'stoken' : stoken,
|
||||
'callback' : promise.resolve
|
||||
});
|
||||
};
|
||||
|
||||
if (window.grecaptcha) {
|
||||
renderWidget();
|
||||
} else {
|
||||
window.grecaptchaLoadCallback = function() {
|
||||
delete window.grecaptchaLoadCallback;
|
||||
renderWidget();
|
||||
};
|
||||
|
||||
$.getScript('https://www.google.com/recaptcha/api.js?onload=grecaptchaLoadCallback&render=explicit')
|
||||
.fail(function() { promise.reject(); });
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var vent = require('vent');
|
||||
var Marionette = require('marionette');
|
||||
var DeleteView = require('../Delete/NotificationDeleteView');
|
||||
|
@ -90,21 +91,45 @@ var view = Marionette.ItemView.extend({
|
|||
this.ui.indicator.show();
|
||||
|
||||
var self = this;
|
||||
var callbackUrl = window.location.origin + '/oauth.html';
|
||||
var fields = this.model.get('fields');
|
||||
var consumerKeyObj = _.findWhere(fields, { name: 'ConsumerKey' });
|
||||
var consumerSecretObj = _.findWhere(fields, { name: 'ConsumerSecret' });
|
||||
var queryParams = {
|
||||
callbackUrl: callbackUrl,
|
||||
consumerKey: (consumerKeyObj ? consumerKeyObj.value : ''),
|
||||
consumerSecret: (consumerSecretObj ? consumerSecretObj.value : '')
|
||||
};
|
||||
|
||||
var promise = this.model.connectData(this.ui.authorizedNotificationButton.data('value'), queryParams);
|
||||
var promise = this.model.requestAction('startOAuth', { callbackUrl: window.location.origin + '/oauth.html' })
|
||||
.then(function(response) {
|
||||
return self._showOAuthWindow(response.oauthUrl);
|
||||
})
|
||||
.then(function(responseQueryParams) {
|
||||
return self.model.requestAction('getOAuthToken', responseQueryParams);
|
||||
})
|
||||
.then(function(response) {
|
||||
self.model.setFieldValue('AccessToken', response.accessToken);
|
||||
self.model.setFieldValue('AccessTokenSecret', response.accessTokenSecret);
|
||||
});
|
||||
|
||||
promise.always(function() {
|
||||
self.ui.indicator.hide();
|
||||
});
|
||||
},
|
||||
|
||||
_showOAuthWindow : function(oauthUrl) {
|
||||
var promise = $.Deferred();
|
||||
|
||||
window.open(oauthUrl);
|
||||
var selfWindow = window;
|
||||
selfWindow.onCompleteOauth = function(query, callback) {
|
||||
delete selfWindow.onCompleteOauth;
|
||||
|
||||
var queryParams = {};
|
||||
var splitQuery = query.substring(1).split('&');
|
||||
_.each(splitQuery, function (param) {
|
||||
var paramSplit = param.split('=');
|
||||
queryParams[paramSplit[0]] = paramSplit[1];
|
||||
});
|
||||
|
||||
callback();
|
||||
|
||||
promise.resolve(queryParams);
|
||||
};
|
||||
|
||||
return promise;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -4,93 +4,38 @@ var DeepModel = require('backbone.deepmodel');
|
|||
var Messenger = require('../Shared/Messenger');
|
||||
|
||||
module.exports = DeepModel.extend({
|
||||
connectData : function(action, initialQueryParams) {
|
||||
|
||||
getFieldValue : function(name) {
|
||||
var index = _.indexOf(_.pluck(this.get('fields'), 'name'), name);
|
||||
return this.get('fields.' + index + '.value');
|
||||
},
|
||||
|
||||
setFieldValue : function(name, value) {
|
||||
var index = _.indexOf(_.pluck(this.get('fields'), 'name'), name);
|
||||
return this.set('fields.' + index + '.value', value);
|
||||
},
|
||||
|
||||
requestAction : function(action, queryParams) {
|
||||
var self = this;
|
||||
|
||||
this.trigger('connect:sync');
|
||||
|
||||
var promise = $.Deferred();
|
||||
|
||||
var callAction = function(action, queryParams) {
|
||||
|
||||
if (queryParams) {
|
||||
action = action + '?' + $.param(queryParams, true);
|
||||
}
|
||||
this.trigger('validation:sync');
|
||||
|
||||
var params = {
|
||||
url : self.collection.url + '/connectData/' + action,
|
||||
url : this.collection.url + '/action/' + action,
|
||||
contentType : 'application/json',
|
||||
data : JSON.stringify(self.toJSON()),
|
||||
data : JSON.stringify(this.toJSON()),
|
||||
type : 'POST',
|
||||
isValidatedCall : true
|
||||
};
|
||||
|
||||
var ajaxPromise = $.ajax(params);
|
||||
ajaxPromise.fail(promise.reject);
|
||||
|
||||
ajaxPromise.success(function(response) {
|
||||
if (response.action)
|
||||
{
|
||||
if (response.action === 'openWindow')
|
||||
{
|
||||
window.open(response.url);
|
||||
var selfWindow = window;
|
||||
|
||||
selfWindow.onCompleteOauth = function(query, callback) {
|
||||
delete selfWindow.onCompleteOauth;
|
||||
|
||||
if (response.nextStep) {
|
||||
var queryParams = {};
|
||||
var splitQuery = query.substring(1).split('&');
|
||||
|
||||
_.each(splitQuery, function (param) {
|
||||
var paramSplit = param.split('=');
|
||||
queryParams[paramSplit[0]] = paramSplit[1];
|
||||
});
|
||||
|
||||
callAction(response.nextStep, _.extend(initialQueryParams, queryParams));
|
||||
}
|
||||
else {
|
||||
promise.resolve(response);
|
||||
if (queryParams) {
|
||||
params.url += '?' + $.param(queryParams, true);
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
else if (response.action === 'updateFields')
|
||||
{
|
||||
_.each(self.get('fields'), function (value, index) {
|
||||
var fieldValue = _.find(response.fields, function (field, key) {
|
||||
return key === value.name;
|
||||
});
|
||||
|
||||
if (fieldValue) {
|
||||
self.set('fields.' + index + '.value', fieldValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response.nextStep) {
|
||||
callAction(response.nextStep, initialQueryParams);
|
||||
}
|
||||
else {
|
||||
promise.resolve(response);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
callAction(action, initialQueryParams);
|
||||
|
||||
Messenger.monitor({
|
||||
promise : promise,
|
||||
successMessage : 'Connecting for \'{0}\' succeeded'.format(this.get('name')),
|
||||
errorMessage : 'Connecting for \'{0}\' failed'.format(this.get('name'))
|
||||
});
|
||||
var promise = $.ajax(params);
|
||||
|
||||
promise.fail(function(response) {
|
||||
self.trigger('connect:failed', response);
|
||||
self.trigger('validation:failed', response);
|
||||
});
|
||||
|
||||
return promise;
|
||||
|
|
|
@ -75,6 +75,8 @@ module.exports = function() {
|
|||
};
|
||||
|
||||
$.fn.removeAllErrors = function() {
|
||||
this.removeClass('has-error');
|
||||
this.removeClass('has-warning');
|
||||
this.find('.has-error').removeClass('has-error');
|
||||
this.find('.has-warning').removeClass('has-warning');
|
||||
this.find('.error').removeClass('error');
|
||||
|
|
Loading…
Reference in New Issue