diff --git a/frontend/src/Components/Form/DeviceInputConnector.js b/frontend/src/Components/Form/DeviceInputConnector.js index d53372b350..43e313826b 100644 --- a/frontend/src/Components/Form/DeviceInputConnector.js +++ b/frontend/src/Components/Form/DeviceInputConnector.js @@ -2,13 +2,13 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { fetchDevices, clearDevices } from 'Store/Actions/deviceActions'; +import { fetchOptions, clearOptions } from 'Store/Actions/providerOptionActions'; import DeviceInput from './DeviceInput'; function createMapStateToProps() { return createSelector( (state, { value }) => value, - (state) => state.devices, + (state) => state.providerOptions, (value, devices) => { return { @@ -37,8 +37,8 @@ function createMapStateToProps() { } const mapDispatchToProps = { - dispatchFetchDevices: fetchDevices, - dispatchClearDevices: clearDevices + dispatchFetchOptions: fetchOptions, + dispatchClearOptions: clearOptions }; class DeviceInputConnector extends Component { @@ -51,7 +51,7 @@ class DeviceInputConnector extends Component { } componentWillUnmount = () => { - // this.props.dispatchClearDevices(); + this.props.dispatchClearOptions(); } // @@ -61,10 +61,14 @@ class DeviceInputConnector extends Component { const { provider, providerData, - dispatchFetchDevices + dispatchFetchOptions } = this.props; - dispatchFetchDevices({ provider, providerData }); + dispatchFetchOptions({ + action: 'getDevices', + provider, + providerData + }); } // @@ -92,8 +96,8 @@ DeviceInputConnector.propTypes = { providerData: PropTypes.object.isRequired, name: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, - dispatchFetchDevices: PropTypes.func.isRequired, - dispatchClearDevices: PropTypes.func.isRequired + dispatchFetchOptions: PropTypes.func.isRequired, + dispatchClearOptions: PropTypes.func.isRequired }; export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector); diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index e2961df13b..d52afb4db9 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -20,7 +20,7 @@ function getType(type) { return inputTypes.NUMBER; case 'path': return inputTypes.PATH; - case 'filepath': + case 'filePath': return inputTypes.PATH; case 'select': return inputTypes.SELECT; @@ -60,6 +60,7 @@ function ProviderFieldFormGroup(props) { value, type, advanced, + hidden, pending, errors, warnings, @@ -68,6 +69,13 @@ function ProviderFieldFormGroup(props) { ...otherProps } = props; + if ( + hidden === 'hidden' || + (hidden === 'hiddenIfNotSet' && !value) + ) { + return null; + } + return ( @@ -108,6 +116,7 @@ ProviderFieldFormGroup.propTypes = { value: PropTypes.any, type: PropTypes.string.isRequired, advanced: PropTypes.bool.isRequired, + hidden: PropTypes.string, pending: PropTypes.bool.isRequired, errors: PropTypes.arrayOf(PropTypes.object).isRequired, warnings: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index 4e367fc898..8e04a20cf1 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -1,10 +1,9 @@ import * as addArtist from './addArtistActions'; import * as app from './appActions'; import * as blacklist from './blacklistActions'; +import * as calendar from './calendarActions'; import * as captcha from './captchaActions'; import * as customFilters from './customFilterActions'; -import * as devices from './deviceActions'; -import * as calendar from './calendarActions'; import * as commands from './commandActions'; import * as albums from './albumActions'; import * as trackFiles from './trackFileActions'; @@ -16,6 +15,7 @@ import * as oAuth from './oAuthActions'; import * as organizePreview from './organizePreviewActions'; import * as retagPreview from './retagPreviewActions'; import * as paths from './pathActions'; +import * as providerOptions from './providerOptionActions'; import * as queue from './queueActions'; import * as releases from './releaseActions'; import * as rootFolders from './rootFolderActions'; @@ -38,7 +38,6 @@ export default [ calendar, commands, customFilters, - devices, albums, trackFiles, albumHistory, @@ -49,6 +48,7 @@ export default [ organizePreview, retagPreview, paths, + providerOptions, queue, releases, rootFolders, diff --git a/frontend/src/Store/Actions/deviceActions.js b/frontend/src/Store/Actions/providerOptionActions.js similarity index 68% rename from frontend/src/Store/Actions/deviceActions.js rename to frontend/src/Store/Actions/providerOptionActions.js index 089d49bf31..c8d05e7e1f 100644 --- a/frontend/src/Store/Actions/deviceActions.js +++ b/frontend/src/Store/Actions/providerOptionActions.js @@ -8,7 +8,7 @@ import { set } from './baseActions'; // // Variables -export const section = 'devices'; +export const section = 'providerOptions'; // // State @@ -23,32 +23,27 @@ export const defaultState = { // // Actions Types -export const FETCH_DEVICES = 'devices/fetchDevices'; -export const CLEAR_DEVICES = 'devices/clearDevices'; +export const FETCH_OPTIONS = 'devices/fetchOptions'; +export const CLEAR_OPTIONS = 'devices/clearOptions'; // // Action Creators -export const fetchDevices = createThunk(FETCH_DEVICES); -export const clearDevices = createAction(CLEAR_DEVICES); +export const fetchOptions = createThunk(FETCH_OPTIONS); +export const clearOptions = createAction(CLEAR_OPTIONS); // // Action Handlers export const actionHandlers = handleThunks({ - [FETCH_DEVICES]: function(getState, payload, dispatch) { - const actionPayload = { - action: 'getDevices', - ...payload - }; - + [FETCH_OPTIONS]: function(getState, payload, dispatch) { dispatch(set({ section, isFetching: true })); - const promise = requestAction(actionPayload); + const promise = requestAction(payload); promise.done((data) => { dispatch(set({ @@ -56,7 +51,7 @@ export const actionHandlers = handleThunks({ isFetching: false, isPopulated: true, error: null, - items: data.devices || [] + items: data.options || [] })); }); @@ -76,7 +71,7 @@ export const actionHandlers = handleThunks({ export const reducers = createHandleActions({ - [CLEAR_DEVICES]: function(state) { + [CLEAR_OPTIONS]: function(state) { return updateSectionState(state, section, defaultState); } diff --git a/src/Lidarr.Http/ClientSchema/Field.cs b/src/Lidarr.Http/ClientSchema/Field.cs index 6aafc89b12..e9363c1f18 100644 --- a/src/Lidarr.Http/ClientSchema/Field.cs +++ b/src/Lidarr.Http/ClientSchema/Field.cs @@ -15,10 +15,11 @@ public class Field public bool Advanced { get; set; } public List SelectOptions { get; set; } public string Section { get; set; } + public string Hidden { get; set; } public Field Clone() { - return (Field) MemberwiseClone(); + return (Field)MemberwiseClone(); } } } diff --git a/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs b/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs index 15b9cc54e9..771d5635c6 100644 --- a/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs @@ -20,14 +20,14 @@ public static List ToSchema(object model) var mappings = GetFieldMappings(model.GetType()); - var result = new List(mappings.Length); + var result = new List(mappings.Length); foreach (var mapping in mappings) { var field = mapping.Field.Clone(); field.Value = mapping.GetterFunc(model); - result.Add(field); + result.Add(field); } return result.OrderBy(r => r.Order).ToList(); @@ -45,7 +45,7 @@ public static object ReadFromSchema(List fields, Type targetType) { var field = fields.Find(f => f.Name == mapping.Field.Name); - mapping.SetterFunc(target, field.Value); + mapping.SetterFunc(target, field.Value); } return target; @@ -54,9 +54,10 @@ public static object ReadFromSchema(List fields, Type targetType) public static T ReadFromSchema(List fields) { - return (T) ReadFromSchema(fields, typeof(T)); + return (T)ReadFromSchema(fields, typeof(T)); } + // Ideally this function should begin a System.Linq.Expression expression tree since it's faster. // But it's probably not needed till performance issues pop up. public static FieldMapping[] GetFieldMappings(Type type) @@ -74,11 +75,12 @@ public static FieldMapping[] GetFieldMappings(Type type) result[i].Field.Order = i; } - _mappings[type] = result; + _mappings[type] = result; } return result; } } + private static FieldMapping[] GetFieldMapping(Type type, string prefix, Func targetSelector) { var result = new List(); @@ -97,7 +99,7 @@ private static FieldMapping[] GetFieldMapping(Type type, string prefix, Func GetSelectOptions(Type selectOptions) { var options = from Enum e in Enum.GetValues(selectOptions) select new SelectOption { Value = Convert.ToInt32(e), Name = e.ToString() }; + return options.OrderBy(o => o.Value).ToList(); } + private static Func GetValueConverter(Type propertyType) { if (propertyType == typeof(int)) @@ -178,13 +187,11 @@ private static List GetSelectOptions(Type selectOptions) { if (fieldValue.GetType() == typeof(JArray)) { - return ((JArray) fieldValue).Select(s => s.Value()); + return ((JArray)fieldValue).Select(s => s.Value()); } - else { - return fieldValue.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) - .Select(s => Convert.ToInt32(s)); + return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => Convert.ToInt32(s)); } }; } @@ -195,11 +202,11 @@ private static List GetSelectOptions(Type selectOptions) { if (fieldValue.GetType() == typeof(JArray)) { - return ((JArray) fieldValue).Select(s => s.Value()); + return ((JArray)fieldValue).Select(s => s.Value()); } else { - return fieldValue.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); + return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } }; } diff --git a/src/NzbDrone.Common/Disk/SystemFolders.cs b/src/NzbDrone.Common/Disk/SystemFolders.cs new file mode 100644 index 0000000000..c108e3d02c --- /dev/null +++ b/src/NzbDrone.Common/Disk/SystemFolders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Common.Disk +{ + public static class SystemFolders + { + public static List GetSystemFolders() + { + if (OsInfo.IsWindows) + { + return new List { Environment.GetFolderPath(Environment.SpecialFolder.Windows) }; + } + + if (OsInfo.IsOsx) + { + return new List { "/System" }; + } + + return new List + { + "/bin", + "/boot", + "/lib", + "/sbin", + "/proc" + }; + } + } +} diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index fc646a273c..c30aba89c3 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -79,6 +79,7 @@ + @@ -259,6 +260,7 @@ 4.0.11 + - \ No newline at end of file + diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index f15f591f56..4f483b4780 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -108,11 +108,7 @@ public void OpenDefaultBrowser(string url) public Process Start(string path, string args = null, StringDictionary environmentVariables = null, Action onOutputDataReceived = null, Action onErrorDataReceived = null) { - if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) - { - args = GetMonoArgs(path, args); - path = "mono"; - } + (path, args) = GetPathAndArgs(path, args); var logger = LogManager.GetLogger(new FileInfo(path).Name); @@ -192,11 +188,7 @@ public Process Start(string path, string args = null, StringDictionary environme public Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null, bool noWindow = false) { - if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) - { - args = GetMonoArgs(path, args); - path = "mono"; - } + (path, args) = GetPathAndArgs(path, args); _logger.Debug("Starting {0} {1}", path, args); @@ -361,9 +353,19 @@ private List GetProcessesByName(string name) return processes; } - private string GetMonoArgs(string path, string args) + private (string Path, string Args) GetPathAndArgs(string path, string args) { - return string.Format("--debug {0} {1}", path, args); + if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) + { + return ("mono", $"--debug {path} {args}"); + } + + if (OsInfo.IsWindows && path.EndsWith(".bat", StringComparison.InvariantCultureIgnoreCase)) + { + return ("cmd.exe", $"/c {path} {args}"); + } + + return (path, args); } } } diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 1da298c1d4..70a9fbe46a 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -19,6 +19,7 @@ public FieldDefinitionAttribute(int order) public bool Advanced { get; set; } public Type SelectOptions { get; set; } public string Section { get; set; } + public HiddenType Hidden { get; set; } } public enum FieldType @@ -30,7 +31,6 @@ public enum FieldType Select, Path, FilePath, - Hidden, Tag, Action, Url, @@ -38,4 +38,11 @@ public enum FieldType OAuth, Device } + + public enum HiddenType + { + Visible, + Hidden, + HiddenIfNotSet + } } diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index a5b92e7c8e..0ff0fc1683 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; @@ -5,6 +6,7 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Processes; using NzbDrone.Common.Serializer; using NzbDrone.Core.Music; @@ -160,6 +162,35 @@ public override ValidationResult Test() failures.Add(new NzbDroneValidationFailure("Path", "File does not exist")); } + foreach (var systemFolder in SystemFolders.GetSystemFolders()) + { + if (systemFolder.IsParentPath(Settings.Path)) + { + failures.Add(new NzbDroneValidationFailure("Path", $"Must not be a descendant of '{systemFolder}'")); + } + } + + if (failures.Empty()) + { + try + { + var environmentVariables = new StringDictionary(); + environmentVariables.Add("Sonarr_EventType", "Test"); + + var processOutput = ExecuteScript(environmentVariables); + + if (processOutput.ExitCode != 0) + { + failures.Add(new NzbDroneValidationFailure(string.Empty, $"Script exited with code: {processOutput.ExitCode}")); + } + } + catch (Exception ex) + { + _logger.Error(ex); + failures.Add(new NzbDroneValidationFailure(string.Empty, ex.Message)); + } + } + return new ValidationResult(failures); } @@ -172,5 +203,10 @@ private void ExecuteScript(StringDictionary environmentVariables) _logger.Debug("Executed external script: {0} - Status: {1}", Settings.Path, process.ExitCode); _logger.Debug($"Script Output: {System.Environment.NewLine}{string.Join(System.Environment.NewLine, process.Lines)}"); } + + private bool ValidatePathParent(string possibleParent, string path) + { + return possibleParent.IsParentPath(path); + } } } diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs index e426ec6513..f4d4d78030 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScriptSettings.cs @@ -11,6 +11,7 @@ public class CustomScriptSettingsValidator : AbstractValidator c.Path).IsValidPath(); + RuleFor(c => c.Arguments).Empty().WithMessage("Arguments are no longer supported for custom scripts"); } } @@ -21,7 +22,7 @@ public class CustomScriptSettings : IProviderConfig [FieldDefinition(0, Label = "Path", Type = FieldType.FilePath)] public string Path { get; set; } - [FieldDefinition(1, Label = "Arguments", HelpText = "Arguments to pass to the script")] + [FieldDefinition(1, Label = "Arguments", HelpText = "Arguments to pass to the script", Hidden = HiddenType.HiddenIfNotSet)] public string Arguments { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheaterSettings.cs b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheaterSettings.cs index c23d409dfa..3306992275 100644 --- a/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheaterSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheaterSettings.cs @@ -13,22 +13,22 @@ public PlexHomeTheaterSettings() //These need to be kept in the same order as XBMC Settings, but we don't want them displayed - [FieldDefinition(2, Label = "Username", Type = FieldType.Hidden)] + [FieldDefinition(2, Label = "Username", Hidden = HiddenType.Hidden)] public new string Username { get; set; } - [FieldDefinition(3, Label = "Password", Type = FieldType.Hidden)] + [FieldDefinition(3, Label = "Password", Hidden = HiddenType.Hidden)] public new string Password { get; set; } - [FieldDefinition(5, Label = "GUI Notification", Type = FieldType.Hidden)] + [FieldDefinition(5, Label = "GUI Notification", Type = FieldType.Checkbox, Hidden = HiddenType.Hidden)] public new bool Notify { get; set; } - [FieldDefinition(6, Label = "Update Library", HelpText = "Update Library on Download & Rename?", Type = FieldType.Hidden)] + [FieldDefinition(6, Label = "Update Library", HelpText = "Update Library on Download & Rename?", Type = FieldType.Checkbox, Hidden = HiddenType.Hidden)] public new bool UpdateLibrary { get; set; } - [FieldDefinition(7, Label = "Clean Library", HelpText = "Clean Library after update?", Type = FieldType.Hidden)] + [FieldDefinition(7, Label = "Clean Library", HelpText = "Clean Library after update?", Type = FieldType.Checkbox, Hidden = HiddenType.Hidden)] public new bool CleanLibrary { get; set; } - [FieldDefinition(8, Label = "Always Update", HelpText = "Update Library even when a video is playing?", Type = FieldType.Hidden)] + [FieldDefinition(8, Label = "Always Update", HelpText = "Update Library even when a video is playing?", Type = FieldType.Checkbox, Hidden = HiddenType.Hidden)] public new bool AlwaysUpdate { get; set; } } } diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs index 843cffd608..bede24023d 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs @@ -70,15 +70,15 @@ public override object RequestAction(string action, IDictionary var devices = _proxy.GetDevices(Settings); return new - { - devices = devices.Where(d => d.Nickname.IsNotNullOrWhiteSpace()) - .OrderBy(d => d.Nickname, StringComparer.InvariantCultureIgnoreCase) - .Select(d => new - { - id = d.Id, - name = d.Nickname - }) - }; + { + options = devices.Where(d => d.Nickname.IsNotNullOrWhiteSpace()) + .OrderBy(d => d.Nickname, StringComparer.InvariantCultureIgnoreCase) + .Select(d => new + { + id = d.Id, + name = d.Nickname + }) + }; } return new { }; diff --git a/src/NzbDrone.Core/Validation/Paths/SystemFolderValidator.cs b/src/NzbDrone.Core/Validation/Paths/SystemFolderValidator.cs index ad321f87a4..d8f3a7f15b 100644 --- a/src/NzbDrone.Core/Validation/Paths/SystemFolderValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/SystemFolderValidator.cs @@ -1,6 +1,5 @@ -using System; using FluentValidation.Validators; -using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Validation.Paths @@ -16,33 +15,13 @@ protected override bool IsValid(PropertyValidatorContext context) { var folder = context.PropertyValue.ToString(); - if (OsInfo.IsWindows) + foreach (var systemFolder in SystemFolders.GetSystemFolders()) { - var windowsFolder = Environment.GetFolderPath(Environment.SpecialFolder.Windows); - context.MessageFormatter.AppendArgument("systemFolder", windowsFolder); - - if (windowsFolder.PathEquals(folder)) - { - context.MessageFormatter.AppendArgument("relationship", "set to"); - - return false; - } - - if (windowsFolder.IsParentPath(folder)) - { - context.MessageFormatter.AppendArgument("relationship", "child of"); - - return false; - } - } - else if (OsInfo.IsOsx) - { - var systemFolder = "/System"; context.MessageFormatter.AppendArgument("systemFolder", systemFolder); if (systemFolder.PathEquals(folder)) { - context.MessageFormatter.AppendArgument("relationship", "child of"); + context.MessageFormatter.AppendArgument("relationship", "set to"); return false; } @@ -54,36 +33,6 @@ protected override bool IsValid(PropertyValidatorContext context) return false; } } - else - { - var folders = new[] - { - "/bin", - "/boot", - "/lib", - "/sbin", - "/proc" - }; - - foreach (var f in folders) - { - context.MessageFormatter.AppendArgument("systemFolder", f); - - if (f.PathEquals(folder)) - { - context.MessageFormatter.AppendArgument("relationship", "child of"); - - return false; - } - - if (f.IsParentPath(folder)) - { - context.MessageFormatter.AppendArgument("relationship", "child of"); - - return false; - } - } - } return true; }