1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-05-11 02:34:08 +02:00

Add tdp:// scheme for plugins (with 'root/' to access root files)

This commit is contained in:
chylex 2020-06-02 12:31:34 +02:00
parent 464e758b94
commit 458eeeccda
9 changed files with 134 additions and 5 deletions

View File

@ -16,6 +16,7 @@
using TweetDuck.Dialogs.Settings;
using TweetDuck.Management;
using TweetDuck.Management.Analytics;
using TweetDuck.Plugins;
using TweetDuck.Updates;
using TweetDuck.Utils;
using TweetLib.Core.Features.Plugins;
@ -65,7 +66,7 @@ public bool IsWaiting{
private VideoPlayer videoPlayer;
private AnalyticsManager analytics;
public FormBrowser(){
public FormBrowser(PluginSchemeFactory pluginScheme){
InitializeComponent();
Text = Program.BrandName;
@ -74,6 +75,7 @@ public FormBrowser(){
this.plugins.Reloaded += plugins_Reloaded;
this.plugins.Executed += plugins_Executed;
this.plugins.Reload();
pluginScheme.Setup(plugins);
this.notification = new FormNotificationTweet(this, plugins);
this.notification.Show();

View File

@ -114,6 +114,8 @@ private void browser_LoadingStateChanged(object sender, LoadingStateChangedEvent
browser.AddWordToDictionary(word);
}
Cef.AddCrossOriginWhitelistEntry(TwitterUrls.TweetDeck, PluginSchemeFactory.Name, "", true);
browser.BeginInvoke(new Action(OnBrowserReady));
browser.LoadingStateChanged -= browser_LoadingStateChanged;
}

View File

@ -0,0 +1,35 @@
using System.Net;
using CefSharp;
using TweetLib.Core.Browser;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Plugins{
sealed class PluginSchemeFactory : ISchemeHandlerFactory{
public const string Name = PluginSchemeHandler<IResourceHandler>.Name;
private readonly PluginSchemeHandler<IResourceHandler> handler = new PluginSchemeHandler<IResourceHandler>(new ResourceProvider());
internal void Setup(PluginManager plugins){
handler.Setup(plugins);
}
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request){
return handler.Process(request.Url);
}
private sealed class ResourceProvider : IResourceProvider<IResourceHandler>{
public IResourceHandler Status(HttpStatusCode code, string message){
return ResourceHandler.ForErrorMessage(message, code);
}
public IResourceHandler File(byte[] bytes, string extension){
if (bytes.Length == 0){
return Status(HttpStatusCode.NoContent, "File is empty."); // FromByteArray crashes CEF internals with no contents
}
else{
return ResourceHandler.FromByteArray(bytes, ResourceHandler.GetMimeType(extension));
}
}
}
}
}

View File

@ -11,6 +11,7 @@
using TweetDuck.Configuration;
using TweetDuck.Dialogs;
using TweetDuck.Management;
using TweetDuck.Plugins;
using TweetDuck.Resources;
using TweetDuck.Utils;
using TweetLib.Core;
@ -168,6 +169,17 @@ private static void Main(){
LogSeverity = Arguments.HasFlag(Arguments.ArgLogging) ? LogSeverity.Info : LogSeverity.Disable
#endif
};
var pluginScheme = new PluginSchemeFactory();
settings.RegisterScheme(new CefCustomScheme{
SchemeName = PluginSchemeFactory.Name,
IsStandard = false,
IsSecure = true,
IsCorsEnabled = true,
IsCSPBypassing = true,
SchemeHandlerFactory = pluginScheme
});
CommandLineArgs.ReadCefArguments(Config.User.CustomCefArgs).ToDictionary(settings.CefCommandLineArgs);
BrowserUtils.SetupCefArgs(settings.CefCommandLineArgs);
@ -176,7 +188,7 @@ private static void Main(){
Win.Application.ApplicationExit += (sender, args) => ExitCleanup();
FormBrowser mainForm = new FormBrowser();
FormBrowser mainForm = new FormBrowser(pluginScheme);
Resources.Initialize(mainForm);
Win.Application.Run(mainForm);

View File

@ -53,6 +53,7 @@
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<Compile Include="Plugins\PluginSchemeFactory.cs" />
<Compile Include="Version.cs" />
<Compile Include="Configuration\Arguments.cs" />
<Compile Include="Configuration\ConfigManager.cs" />

View File

@ -0,0 +1,8 @@
using System.Net;
namespace TweetLib.Core.Browser{
public interface IResourceProvider<T>{
T Status(HttpStatusCode code, string message);
T File(byte[] bytes, string extension);
}
}

View File

@ -47,7 +47,7 @@ internal int GetTokenFromPlugin(Plugin plugin){
return token;
}
private Plugin? GetPluginFromToken(int token){
internal Plugin? GetPluginFromToken(int token){
return tokens.TryGetValue(token, out Plugin plugin) ? plugin : null;
}

View File

@ -16,14 +16,14 @@ public sealed class PluginManager{
public IEnumerable<InjectedHTML> NotificationInjections => bridge.NotificationInjections;
public IPluginConfig Config { get; }
public event EventHandler<PluginErrorEventArgs>? Reloaded;
public event EventHandler<PluginErrorEventArgs>? Executed;
private readonly string pluginFolder;
private readonly string pluginDataFolder;
private readonly PluginBridge bridge;
internal readonly PluginBridge bridge;
private IScriptExecutor? browserExecutor;
private readonly HashSet<Plugin> plugins = new HashSet<Plugin>();

View File

@ -0,0 +1,69 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using TweetLib.Core.Browser;
using TweetLib.Core.Features.Plugins.Enums;
namespace TweetLib.Core.Features.Plugins{
public sealed class PluginSchemeHandler<T> where T : class{
public const string Name = "tdp";
private readonly IResourceProvider<T> resourceProvider;
private PluginBridge? bridge = null;
public PluginSchemeHandler(IResourceProvider<T> resourceProvider){
this.resourceProvider = resourceProvider;
}
public void Setup(PluginManager plugins){
if (this.bridge != null){
throw new InvalidOperationException("Plugin scheme handler is already setup.");
}
this.bridge = plugins.bridge;
}
public T? Process(string url){
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Scheme != Name || !int.TryParse(uri.Authority, out var identifier)){
return null;
}
var segments = uri.Segments.Select(segment => segment.TrimEnd('/')).Where(segment => !string.IsNullOrEmpty(segment)).ToArray();
if (segments.Length > 0){
var handler = segments[0] switch{
"root" => DoReadRootFile(identifier, segments),
_ => null
};
if (handler != null){
return handler;
}
}
return resourceProvider.Status(HttpStatusCode.BadRequest, "Bad URL path: " + uri.AbsolutePath);
}
private T? DoReadRootFile(int identifier, string[] segments){
string path = string.Join("/", segments, 1, segments.Length - 1);
Plugin? plugin = bridge?.GetPluginFromToken(identifier);
string fullPath = plugin == null ? string.Empty : plugin.GetFullPathIfSafe(PluginFolder.Root, path);
if (fullPath.Length == 0){
return resourceProvider.Status(HttpStatusCode.Forbidden, "File path has to be relative to the plugin root folder.");
}
try{
return resourceProvider.File(File.ReadAllBytes(fullPath), Path.GetExtension(path));
}catch(FileNotFoundException){
return resourceProvider.Status(HttpStatusCode.NotFound, "File not found.");
}catch(DirectoryNotFoundException){
return resourceProvider.Status(HttpStatusCode.NotFound, "Directory not found.");
}catch(Exception e){
return resourceProvider.Status(HttpStatusCode.InternalServerError, e.Message);
}
}
}
}