diff --git a/Browser/Adapters/CefBrowserComponent.cs b/Browser/Adapters/CefBrowserComponent.cs deleted file mode 100644 index 88e19cb2..00000000 --- a/Browser/Adapters/CefBrowserComponent.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.IO; -using CefSharp; -using CefSharp.WinForms; -using TweetDuck.Browser.Handling; -using TweetDuck.Management; -using TweetDuck.Utils; -using TweetLib.Browser.Base; -using TweetLib.Browser.Events; -using TweetLib.Browser.Interfaces; -using TweetLib.Utils.Static; -using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler; -using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler; - -namespace TweetDuck.Browser.Adapters { - abstract class CefBrowserComponent : IBrowserComponent { - public bool Ready { get; private set; } - - public string Url => browser.Address; - public string CacheFolder => BrowserCache.CacheFolder; - - public event EventHandler<BrowserLoadedEventArgs> BrowserLoaded; - public event EventHandler<PageLoadEventArgs> PageLoadStart; - public event EventHandler<PageLoadEventArgs> PageLoadEnd; - - private readonly ChromiumWebBrowser browser; - - protected CefBrowserComponent(ChromiumWebBrowser browser) { - this.browser = browser; - this.browser.JsDialogHandler = new JavaScriptDialogHandler(); - this.browser.LifeSpanHandler = new CustomLifeSpanHandler(); - this.browser.LoadingStateChanged += OnLoadingStateChanged; - this.browser.LoadError += OnLoadError; - this.browser.FrameLoadStart += OnFrameLoadStart; - this.browser.FrameLoadEnd += OnFrameLoadEnd; - this.browser.SetupZoomEvents(); - } - - void IBrowserComponent.Setup(BrowserSetup setup) { - browser.MenuHandler = SetupContextMenu(setup.ContextMenuHandler); - browser.ResourceRequestHandlerFactory = SetupResourceHandlerFactory(setup.ResourceRequestHandler); - } - - protected abstract ContextMenuBase SetupContextMenu(IContextMenuHandler handler); - - protected abstract CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler); - - private void OnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e) { - if (!e.IsLoading) { - Ready = true; - browser.LoadingStateChanged -= OnLoadingStateChanged; - BrowserLoaded?.Invoke(this, new BrowserLoadedEventArgsImpl(browser)); - BrowserLoaded = null; - } - } - - private sealed class BrowserLoadedEventArgsImpl : BrowserLoadedEventArgs { - private readonly IWebBrowser browser; - - public BrowserLoadedEventArgsImpl(IWebBrowser browser) { - this.browser = browser; - } - - public override void AddDictionaryWords(params string[] words) { - foreach (string word in words) { - browser.AddWordToDictionary(word); - } - } - } - - private void OnLoadError(object sender, LoadErrorEventArgs e) { - if (e.ErrorCode == CefErrorCode.Aborted) { - return; - } - - if (!e.FailedUrl.StartsWithOrdinal("td://resources/error/")) { - string errorName = Enum.GetName(typeof(CefErrorCode), e.ErrorCode); - string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty); - browser.Load("td://resources/error/error.html#" + Uri.EscapeDataString(errorTitle)); - } - } - - private void OnFrameLoadStart(object sender, FrameLoadStartEventArgs e) { - if (e.Frame.IsMain) { - PageLoadStart?.Invoke(this, new PageLoadEventArgs(e.Url)); - } - } - - private void OnFrameLoadEnd(object sender, FrameLoadEndEventArgs e) { - if (e.Frame.IsMain) { - PageLoadEnd?.Invoke(this, new PageLoadEventArgs(e.Url)); - } - } - - public void AttachBridgeObject(string name, object bridge) { - browser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true; - browser.JavascriptObjectRepository.Register(name, bridge, isAsync: true, BindingOptions.DefaultBinder); - } - - public void RunScript(string identifier, string script) { - using IFrame frame = browser.GetMainFrame(); - frame.ExecuteJavaScriptAsync(script, identifier, 1); - } - - public void DownloadFile(string url, string path, Action onSuccess, Action<Exception> onError) { - Cef.UIThreadTaskFactory.StartNew(() => { - try { - using IFrame frame = browser.GetMainFrame(); - var request = frame.CreateRequest(false); - - request.Method = "GET"; - request.Url = url; - request.Flags = UrlRequestFlags.AllowStoredCredentials; - request.SetReferrer(Url, ReferrerPolicy.Default); - - var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read); - var client = new DownloadRequestClient(fileStream, onSuccess, onError); - frame.CreateUrlRequest(request, client); - } catch (Exception e) { - onError?.Invoke(e); - } - }); - } - } -} diff --git a/Browser/Adapters/CefContextMenuActionRegistry.cs b/Browser/Adapters/CefContextMenuActionRegistry.cs deleted file mode 100644 index ae07c9dc..00000000 --- a/Browser/Adapters/CefContextMenuActionRegistry.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using CefSharp; - -namespace TweetDuck.Browser.Adapters { - sealed class CefContextMenuActionRegistry { - private readonly Dictionary<CefMenuCommand, Action> actions = new Dictionary<CefMenuCommand, Action>(); - - public CefMenuCommand AddAction(Action action) { - CefMenuCommand id = CefMenuCommand.UserFirst + 500 + actions.Count; - actions[id] = action; - return id; - } - - public bool Execute(CefMenuCommand id) { - if (actions.TryGetValue(id, out var action)) { - action(); - return true; - } - - return false; - } - - public void Clear() { - actions.Clear(); - } - } -} diff --git a/Browser/Adapters/CefResourceHandlerFactory.cs b/Browser/Adapters/CefResourceHandlerFactory.cs deleted file mode 100644 index ad41974e..00000000 --- a/Browser/Adapters/CefResourceHandlerFactory.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using CefSharp; -using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler; - -namespace TweetDuck.Browser.Adapters { - sealed class CefResourceHandlerFactory : IResourceRequestHandlerFactory { - bool IResourceRequestHandlerFactory.HasHandlers => registry != null; - - private readonly CefResourceRequestHandler resourceRequestHandler; - private readonly CefResourceHandlerRegistry registry; - - public CefResourceHandlerFactory(IResourceRequestHandler resourceRequestHandler, CefResourceHandlerRegistry registry) { - this.resourceRequestHandler = new CefResourceRequestHandler(registry, resourceRequestHandler); - this.registry = registry; - } - - [SuppressMessage("ReSharper", "RedundantAssignment")] - CefSharp.IResourceRequestHandler IResourceRequestHandlerFactory.GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) { - disableDefaultHandling = registry != null && registry.HasHandler(request.Url); - return resourceRequestHandler; - } - } -} diff --git a/Browser/Adapters/CefResourceHandlerRegistry.cs b/Browser/Adapters/CefResourceHandlerRegistry.cs deleted file mode 100644 index b0e97e6b..00000000 --- a/Browser/Adapters/CefResourceHandlerRegistry.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Text; -using CefSharp; - -namespace TweetDuck.Browser.Adapters { - sealed class CefResourceHandlerRegistry { - private readonly ConcurrentDictionary<string, Func<IResourceHandler>> resourceHandlers = new ConcurrentDictionary<string, Func<IResourceHandler>>(StringComparer.OrdinalIgnoreCase); - - public bool HasHandler(string url) { - return resourceHandlers.ContainsKey(url); - } - - public IResourceHandler GetHandler(string url) { - return resourceHandlers.TryGetValue(url, out var handler) ? handler() : null; - } - - private void Register(string url, Func<IResourceHandler> factory) { - if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) { - throw new ArgumentException("Resource handler URL must be absolute!"); - } - - resourceHandlers.AddOrUpdate(uri.AbsoluteUri, factory, (key, prev) => factory); - } - - public void RegisterStatic(string url, byte[] staticData, string mimeType = ResourceHandler.DefaultMimeType) { - Register(url, () => ResourceHandler.FromByteArray(staticData, mimeType)); - } - - public void RegisterStatic(string url, string staticData, string mimeType = ResourceHandler.DefaultMimeType) { - Register(url, () => ResourceHandler.FromString(staticData, Encoding.UTF8, mimeType: mimeType)); - } - - public void RegisterDynamic(string url, IResourceHandler handler) { - Register(url, () => handler); - } - - public void Unregister(string url) { - resourceHandlers.TryRemove(url, out _); - } - } -} diff --git a/Browser/Adapters/CefResourceRequestHandler.cs b/Browser/Adapters/CefResourceRequestHandler.cs deleted file mode 100644 index 71915076..00000000 --- a/Browser/Adapters/CefResourceRequestHandler.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Collections.Generic; -using CefSharp; -using CefSharp.Handler; -using TweetDuck.Browser.Handling; -using TweetLib.Browser.Interfaces; -using TweetLib.Browser.Request; -using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler; -using ResourceType = TweetLib.Browser.Request.ResourceType; - -namespace TweetDuck.Browser.Adapters { - sealed class CefResourceRequestHandler : ResourceRequestHandler { - private readonly CefResourceHandlerRegistry resourceHandlerRegistry; - private readonly IResourceRequestHandler resourceRequestHandler; - private readonly Dictionary<ulong, IResponseProcessor> responseProcessors = new Dictionary<ulong, IResponseProcessor>(); - - public CefResourceRequestHandler(CefResourceHandlerRegistry resourceHandlerRegistry, IResourceRequestHandler resourceRequestHandler) { - this.resourceHandlerRegistry = resourceHandlerRegistry; - this.resourceRequestHandler = resourceRequestHandler; - } - - protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) { - if (request.ResourceType == CefSharp.ResourceType.CspReport) { - callback.Dispose(); - return CefReturnValue.Cancel; - } - - if (resourceRequestHandler != null) { - var result = resourceRequestHandler.Handle(request.Url, TranslateResourceType(request.ResourceType)); - - switch (result) { - case RequestHandleResult.Redirect redirect: - request.Url = redirect.Url; - break; - - case RequestHandleResult.Process process: - request.SetHeaderByName("Accept-Encoding", "identity", overwrite: true); - responseProcessors[request.Identifier] = process.Processor; - break; - - case RequestHandleResult.Cancel _: - callback.Dispose(); - return CefReturnValue.Cancel; - } - } - - return base.OnBeforeResourceLoad(chromiumWebBrowser, browser, frame, request, callback); - } - - protected override IResourceHandler GetResourceHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request) { - return resourceHandlerRegistry?.GetHandler(request.Url); - } - - protected override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) { - if (responseProcessors.TryGetValue(request.Identifier, out var processor) && int.TryParse(response.Headers["Content-Length"], out int totalBytes)) { - return new ResponseFilter(processor, totalBytes); - } - - return base.GetResourceResponseFilter(browserControl, browser, frame, request, response); - } - - protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength) { - responseProcessors.Remove(request.Identifier); - base.OnResourceLoadComplete(chromiumWebBrowser, browser, frame, request, response, status, receivedContentLength); - } - - private static ResourceType TranslateResourceType(CefSharp.ResourceType resourceType) { - return resourceType switch { - CefSharp.ResourceType.MainFrame => ResourceType.MainFrame, - CefSharp.ResourceType.Script => ResourceType.Script, - CefSharp.ResourceType.Stylesheet => ResourceType.Stylesheet, - CefSharp.ResourceType.Xhr => ResourceType.Xhr, - CefSharp.ResourceType.Image => ResourceType.Image, - _ => ResourceType.Unknown - }; - } - } -} diff --git a/Browser/Adapters/CefSchemeResourceVisitor.cs b/Browser/Adapters/CefSchemeResourceVisitor.cs deleted file mode 100644 index 1495e861..00000000 --- a/Browser/Adapters/CefSchemeResourceVisitor.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.IO; -using System.Net; -using CefSharp; -using TweetLib.Browser.Interfaces; -using TweetLib.Browser.Request; - -namespace TweetDuck.Browser.Adapters { - sealed class CefSchemeResourceVisitor : ISchemeResourceVisitor<IResourceHandler> { - public static CefSchemeResourceVisitor Instance { get; } = new CefSchemeResourceVisitor(); - - private static readonly SchemeResource.Status FileIsEmpty = new SchemeResource.Status(HttpStatusCode.NoContent, "File is empty."); - - private CefSchemeResourceVisitor() {} - - public IResourceHandler Status(SchemeResource.Status status) { - var handler = CreateHandler(Array.Empty<byte>()); - handler.StatusCode = (int) status.Code; - handler.StatusText = status.Message; - return handler; - } - - public IResourceHandler File(SchemeResource.File file) { - byte[] contents = file.Contents; - if (contents.Length == 0) { - return Status(FileIsEmpty); // FromByteArray crashes CEF internals with no contents - } - - var handler = CreateHandler(contents); - handler.MimeType = Cef.GetMimeType(file.Extension); - return handler; - } - - private static ResourceHandler CreateHandler(byte[] bytes) { - return ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true); - } - } -} diff --git a/Browser/Base/CefBrowserAdapter.cs b/Browser/Base/CefBrowserAdapter.cs new file mode 100644 index 00000000..456d06a4 --- /dev/null +++ b/Browser/Base/CefBrowserAdapter.cs @@ -0,0 +1,20 @@ +using CefSharp; +using CefSharp.WinForms; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetDuck.Browser.Base { + sealed class CefBrowserAdapter : IBrowserWrapper<IFrame> { + public string Url => browser.Address; + public IFrame MainFrame => browser.GetMainFrame(); + + private readonly ChromiumWebBrowser browser; + + public CefBrowserAdapter(ChromiumWebBrowser browser) { + this.browser = browser; + } + + public void AddWordToDictionary(string word) { + browser.AddWordToDictionary(word); + } + } +} diff --git a/Browser/Base/CefBrowserComponent.cs b/Browser/Base/CefBrowserComponent.cs new file mode 100644 index 00000000..fd25638f --- /dev/null +++ b/Browser/Base/CefBrowserComponent.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using CefSharp; +using CefSharp.WinForms; +using TweetDuck.Browser.Handling; +using TweetDuck.Management; +using TweetDuck.Utils; +using TweetLib.Browser.Base; +using TweetLib.Browser.CEF.Component; +using TweetLib.Browser.CEF.Data; +using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler; + +namespace TweetDuck.Browser.Base { + sealed class CefBrowserComponent : BrowserComponent<IFrame> { + public delegate ContextMenuBase CreateContextMenu(IContextMenuHandler handler); + + private static readonly CreateContextMenu DefaultContextMenuFactory = handler => new ContextMenuBase(handler); + + public override string CacheFolder => BrowserCache.CacheFolder; + + public ResourceHandlerRegistry<IResourceHandler> ResourceHandlerRegistry { get; } = new ResourceHandlerRegistry<IResourceHandler>(CefResourceHandlerFactory.Instance); + + private readonly ChromiumWebBrowser browser; + private readonly bool autoReload; + + private CreateContextMenu createContextMenu; + + public CefBrowserComponent(ChromiumWebBrowser browser, CreateContextMenu createContextMenu = null, bool autoReload = true) : base(new CefBrowserAdapter(browser), CefFrameAdapter.Instance) { + this.browser = browser; + this.browser.LoadingStateChanged += OnLoadingStateChanged; + this.browser.LoadError += OnLoadError; + this.browser.FrameLoadStart += OnFrameLoadStart; + this.browser.FrameLoadEnd += OnFrameLoadEnd; + this.browser.SetupZoomEvents(); + this.createContextMenu = createContextMenu ?? DefaultContextMenuFactory; + this.autoReload = autoReload; + } + + public override void Setup(BrowserSetup setup) { + var lifeSpanHandler = new CefLifeSpanHandler(PopupHandler.Instance); + var requestHandler = new CefRequestHandler(lifeSpanHandler, autoReload); + + browser.DragHandler = new CefDragHandler(requestHandler, this); + browser.JsDialogHandler = new CustomJsDialogHandler(); + browser.LifeSpanHandler = lifeSpanHandler; + browser.MenuHandler = createContextMenu(setup.ContextMenuHandler); + browser.RequestHandler = requestHandler; + browser.ResourceRequestHandlerFactory = new CefResourceRequestHandlerFactory(setup.ResourceRequestHandler, ResourceHandlerRegistry); + + createContextMenu = null; + } + + public override void AttachBridgeObject(string name, object bridge) { + browser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true; + browser.JavascriptObjectRepository.Register(name, bridge, isAsync: true, BindingOptions.DefaultBinder); + } + + private void OnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e) { + base.OnLoadingStateChanged(e.IsLoading); + } + + private void OnLoadError(object sender, LoadErrorEventArgs e) { + base.OnLoadError(e.FailedUrl, e.ErrorCode, CefErrorCodeAdapter.Instance); + } + + private void OnFrameLoadStart(object sender, FrameLoadStartEventArgs e) { + base.OnFrameLoadStart(e.Url, e.Frame); + } + + private void OnFrameLoadEnd(object sender, FrameLoadEndEventArgs e) { + base.OnFrameLoadEnd(e.Url, e.Frame); + } + + public override void DownloadFile(string url, string path, Action onSuccess, Action<Exception> onError) { + Cef.UIThreadTaskFactory.StartNew(() => { + try { + using IFrame frame = browser.GetMainFrame(); + var request = frame.CreateRequest(false); + + request.Method = "GET"; + request.Url = url; + request.Flags = UrlRequestFlags.AllowStoredCredentials; + request.SetReferrer(Url, ReferrerPolicy.Default); + + var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read); + var client = new CefDownloadRequestClient(fileStream, onSuccess, onError); + frame.CreateUrlRequest(request, client); + } catch (Exception e) { + onError?.Invoke(e); + } + }); + } + } +} diff --git a/Browser/Base/CefByteArrayResourceHandler.cs b/Browser/Base/CefByteArrayResourceHandler.cs new file mode 100644 index 00000000..059f49dc --- /dev/null +++ b/Browser/Base/CefByteArrayResourceHandler.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using CefSharp; +using CefSharp.Callback; +using TweetLib.Browser.CEF.Data; +using TweetLib.Browser.CEF.Logic; + +namespace TweetDuck.Browser.Base { + sealed class CefByteArrayResourceHandler : IResourceHandler { + private static readonly ByteArrayResourceHandlerLogic<IResponse>.WriteToOut<Stream> WriteToOut = delegate (Stream dataOut, byte[] dataIn, int position, int length) { + dataOut.Write(dataIn, position, length); + }; + + private ByteArrayResourceHandlerLogic<IResponse> logic; + + public CefByteArrayResourceHandler() { + SetResource(new ByteArrayResource(Array.Empty<byte>())); + } + + public CefByteArrayResourceHandler(ByteArrayResource resource) { + SetResource(resource); + } + + public void SetResource(ByteArrayResource resource) { + this.logic = new ByteArrayResourceHandlerLogic<IResponse>(resource, CefResponseAdapter.Instance); + } + + bool IResourceHandler.Open(IRequest request, out bool handleRequest, ICallback callback) { + return logic.Open(out handleRequest); + } + + void IResourceHandler.GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl) { + logic.GetResponseHeaders(response, out responseLength, out redirectUrl); + } + + bool IResourceHandler.Skip(long bytesToSkip, out long bytesSkipped, IResourceSkipCallback callback) { + return logic.Skip(bytesToSkip, out bytesSkipped, callback); + } + + bool IResourceHandler.Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback) { + return logic.Read(WriteToOut, dataOut, out bytesRead, callback); + } + + bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback) { + throw new NotSupportedException(); + } + + bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback) { + throw new NotSupportedException(); + } + + void IResourceHandler.Cancel() {} + void IDisposable.Dispose() {} + } +} diff --git a/Browser/Adapters/CefContextMenuModel.cs b/Browser/Base/CefContextMenuModel.cs similarity index 90% rename from Browser/Adapters/CefContextMenuModel.cs rename to Browser/Base/CefContextMenuModel.cs index 77f8fc9a..b4dc2551 100644 --- a/Browser/Adapters/CefContextMenuModel.cs +++ b/Browser/Base/CefContextMenuModel.cs @@ -1,16 +1,17 @@ using System; using CefSharp; +using TweetLib.Browser.CEF.Data; using TweetLib.Browser.Contexts; using TweetLib.Browser.Interfaces; using TweetLib.Core.Features.TweetDeck; using TweetLib.Core.Features.Twitter; -namespace TweetDuck.Browser.Adapters { +namespace TweetDuck.Browser.Base { sealed class CefContextMenuModel : IContextMenuBuilder { private readonly IMenuModel model; - private readonly CefContextMenuActionRegistry actionRegistry; + private readonly ContextMenuActionRegistry<CefMenuCommand> actionRegistry; - public CefContextMenuModel(IMenuModel model, CefContextMenuActionRegistry actionRegistry) { + public CefContextMenuModel(IMenuModel model, ContextMenuActionRegistry<CefMenuCommand> actionRegistry) { this.model = model; this.actionRegistry = actionRegistry; } diff --git a/Browser/Base/CefDownloadRequestClient.cs b/Browser/Base/CefDownloadRequestClient.cs new file mode 100644 index 00000000..2a3a0954 --- /dev/null +++ b/Browser/Base/CefDownloadRequestClient.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using CefSharp; +using TweetLib.Browser.CEF.Logic; +using static TweetLib.Browser.CEF.Logic.DownloadRequestClientLogic.RequestStatus; + +namespace TweetDuck.Browser.Base { + sealed class CefDownloadRequestClient : UrlRequestClient { + private readonly DownloadRequestClientLogic logic; + + public CefDownloadRequestClient(FileStream fileStream, Action onSuccess, Action<Exception> onError) { + this.logic = new DownloadRequestClientLogic(fileStream, onSuccess, onError); + } + + protected override bool GetAuthCredentials(bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) { + return logic.GetAuthCredentials(callback); + } + + protected override void OnDownloadData(IUrlRequest request, Stream data) { + logic.OnDownloadData(data); + } + + protected override void OnRequestComplete(IUrlRequest request) { + logic.OnRequestComplete(request.RequestStatus switch { + UrlRequestStatus.Success => Success, + UrlRequestStatus.Failed => Failed, + _ => Unknown + }); + } + } +} diff --git a/Browser/Base/CefDragDataAdapter.cs b/Browser/Base/CefDragDataAdapter.cs new file mode 100644 index 00000000..89fdc1c1 --- /dev/null +++ b/Browser/Base/CefDragDataAdapter.cs @@ -0,0 +1,26 @@ +using CefSharp; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetDuck.Browser.Base { + sealed class CefDragDataAdapter : IDragDataAdapter<IDragData> { + public static CefDragDataAdapter Instance { get; } = new CefDragDataAdapter(); + + private CefDragDataAdapter() {} + + public bool IsLink(IDragData data) { + return data.IsLink; + } + + public string GetLink(IDragData data) { + return data.LinkUrl; + } + + public bool IsFragment(IDragData data) { + return data.IsFragment; + } + + public string GetFragmentAsText(IDragData data) { + return data.FragmentText; + } + } +} diff --git a/Browser/Base/CefDragHandler.cs b/Browser/Base/CefDragHandler.cs new file mode 100644 index 00000000..09b1a679 --- /dev/null +++ b/Browser/Base/CefDragHandler.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using CefSharp; +using CefSharp.Enums; +using TweetLib.Browser.CEF.Logic; +using TweetLib.Browser.Interfaces; + +namespace TweetDuck.Browser.Base { + sealed class CefDragHandler : IDragHandler { + private readonly DragHandlerLogic<IDragData, IRequest> logic; + + public CefDragHandler(CefRequestHandler requestHandler, IScriptExecutor executor) { + this.logic = new DragHandlerLogic<IDragData, IRequest>(executor, requestHandler.Logic, CefDragDataAdapter.Instance); + } + + public bool OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask) { + return logic.OnDragEnter(dragData); + } + + public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IFrame frame, IList<DraggableRegion> regions) {} + } +} diff --git a/Browser/Base/CefErrorCodeAdapter.cs b/Browser/Base/CefErrorCodeAdapter.cs new file mode 100644 index 00000000..85876246 --- /dev/null +++ b/Browser/Base/CefErrorCodeAdapter.cs @@ -0,0 +1,19 @@ +using System; +using CefSharp; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetDuck.Browser.Base { + sealed class CefErrorCodeAdapter : IErrorCodeAdapter<CefErrorCode> { + public static CefErrorCodeAdapter Instance { get; } = new CefErrorCodeAdapter(); + + private CefErrorCodeAdapter() {} + + public bool IsAborted(CefErrorCode errorCode) { + return errorCode == CefErrorCode.Aborted; + } + + public string GetName(CefErrorCode errorCode) { + return Enum.GetName(typeof(CefErrorCode), errorCode); + } + } +} diff --git a/Browser/Base/CefFrameAdapter.cs b/Browser/Base/CefFrameAdapter.cs new file mode 100644 index 00000000..d78a32ed --- /dev/null +++ b/Browser/Base/CefFrameAdapter.cs @@ -0,0 +1,26 @@ +using CefSharp; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetDuck.Browser.Base { + sealed class CefFrameAdapter : IFrameAdapter<IFrame> { + public static CefFrameAdapter Instance { get; } = new CefFrameAdapter(); + + private CefFrameAdapter() {} + + public bool IsValid(IFrame frame) { + return frame.IsValid; + } + + public bool IsMain(IFrame frame) { + return frame.IsMain; + } + + public void LoadUrl(IFrame frame, string url) { + frame.LoadUrl(url); + } + + public void ExecuteJavaScriptAsync(IFrame frame, string script, string identifier, int startLine = 1) { + frame.ExecuteJavaScriptAsync(script, identifier, startLine); + } + } +} diff --git a/Browser/Base/CefLifeSpanHandler.cs b/Browser/Base/CefLifeSpanHandler.cs new file mode 100644 index 00000000..28f5c76b --- /dev/null +++ b/Browser/Base/CefLifeSpanHandler.cs @@ -0,0 +1,34 @@ +using CefSharp; +using CefSharp.Handler; +using TweetLib.Browser.CEF.Interfaces; +using TweetLib.Browser.CEF.Logic; +using static TweetLib.Browser.CEF.Logic.LifeSpanHandlerLogic.TargetDisposition; + +namespace TweetDuck.Browser.Base { + sealed class CefLifeSpanHandler : LifeSpanHandler { + public LifeSpanHandlerLogic Logic { get; } + + public CefLifeSpanHandler(IPopupHandler popupHandler) { + this.Logic = new LifeSpanHandlerLogic(popupHandler); + } + + protected override bool OnBeforePopup(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures, IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser) { + newBrowser = null; + return Logic.OnBeforePopup(targetUrl, ConvertTargetDisposition(targetDisposition)); + } + + protected override bool DoClose(IWebBrowser chromiumWebBrowser, IBrowser browser) { + return Logic.DoClose(); + } + + public static LifeSpanHandlerLogic.TargetDisposition ConvertTargetDisposition(WindowOpenDisposition targetDisposition) { + return targetDisposition switch { + WindowOpenDisposition.NewBackgroundTab => NewBackgroundTab, + WindowOpenDisposition.NewForegroundTab => NewForegroundTab, + WindowOpenDisposition.NewPopup => NewPopup, + WindowOpenDisposition.NewWindow => NewWindow, + _ => Other + }; + } + } +} diff --git a/Browser/Base/CefRequestAdapter.cs b/Browser/Base/CefRequestAdapter.cs new file mode 100644 index 00000000..2722d6b7 --- /dev/null +++ b/Browser/Base/CefRequestAdapter.cs @@ -0,0 +1,46 @@ +using CefSharp; +using TweetLib.Browser.CEF.Interfaces; +using ResourceType = TweetLib.Browser.Request.ResourceType; + +namespace TweetDuck.Browser.Base { + sealed class CefRequestAdapter : IRequestAdapter<IRequest> { + public static CefRequestAdapter Instance { get; } = new CefRequestAdapter(); + + private CefRequestAdapter() {} + + public ulong GetIdentifier(IRequest request) { + return request.Identifier; + } + + public string GetUrl(IRequest request) { + return request.Url; + } + + public void SetUrl(IRequest request, string url) { + request.Url = url; + } + + public bool IsTransitionForwardBack(IRequest request) { + return request.TransitionType.HasFlag(TransitionType.ForwardBack); + } + + public bool IsCspReport(IRequest request) { + return request.ResourceType == CefSharp.ResourceType.CspReport; + } + + public ResourceType GetResourceType(IRequest request) { + return request.ResourceType switch { + CefSharp.ResourceType.MainFrame => ResourceType.MainFrame, + CefSharp.ResourceType.Script => ResourceType.Script, + CefSharp.ResourceType.Stylesheet => ResourceType.Stylesheet, + CefSharp.ResourceType.Xhr => ResourceType.Xhr, + CefSharp.ResourceType.Image => ResourceType.Image, + _ => ResourceType.Unknown + }; + } + + public void SetHeader(IRequest request, string header, string value) { + request.SetHeaderByName(header, value, overwrite: true); + } + } +} diff --git a/Browser/Base/CefRequestHandler.cs b/Browser/Base/CefRequestHandler.cs new file mode 100644 index 00000000..070ee897 --- /dev/null +++ b/Browser/Base/CefRequestHandler.cs @@ -0,0 +1,30 @@ +using CefSharp; +using CefSharp.Handler; +using TweetLib.Browser.CEF.Logic; + +namespace TweetDuck.Browser.Base { + sealed class CefRequestHandler : RequestHandler { + public RequestHandlerLogic<IRequest> Logic { get; } + + private readonly bool autoReload; + + public CefRequestHandler(CefLifeSpanHandler lifeSpanHandler, bool autoReload) { + this.Logic = new RequestHandlerLogic<IRequest>(CefRequestAdapter.Instance, lifeSpanHandler.Logic); + this.autoReload = autoReload; + } + + protected override bool OnBeforeBrowse(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect) { + return Logic.OnBeforeBrowse(request, userGesture); + } + + protected override bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture) { + return Logic.OnOpenUrlFromTab(targetUrl, userGesture, CefLifeSpanHandler.ConvertTargetDisposition(targetDisposition)); + } + + protected override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status) { + if (autoReload) { + browser.Reload(); + } + } + } +} diff --git a/Browser/Base/CefResourceHandlerFactory.cs b/Browser/Base/CefResourceHandlerFactory.cs new file mode 100644 index 00000000..c087df21 --- /dev/null +++ b/Browser/Base/CefResourceHandlerFactory.cs @@ -0,0 +1,19 @@ +using CefSharp; +using TweetLib.Browser.CEF.Data; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetDuck.Browser.Base { + sealed class CefResourceHandlerFactory : IResourceHandlerFactory<IResourceHandler> { + public static CefResourceHandlerFactory Instance { get; } = new CefResourceHandlerFactory(); + + private CefResourceHandlerFactory() {} + + public IResourceHandler CreateResourceHandler(ByteArrayResource resource) { + return new CefByteArrayResourceHandler(resource); + } + + public string GetMimeTypeFromExtension(string extension) { + return Cef.GetMimeType(extension); + } + } +} diff --git a/Browser/Base/CefResourceRequestHandler.cs b/Browser/Base/CefResourceRequestHandler.cs new file mode 100644 index 00000000..4925810b --- /dev/null +++ b/Browser/Base/CefResourceRequestHandler.cs @@ -0,0 +1,32 @@ +using CefSharp; +using CefSharp.Handler; +using TweetLib.Browser.CEF.Data; +using TweetLib.Browser.CEF.Logic; +using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler; + +namespace TweetDuck.Browser.Base { + sealed class CefResourceRequestHandler : ResourceRequestHandler { + private readonly ResourceRequestHandlerLogic<IRequest, IResponse, IResourceHandler> logic; + + public CefResourceRequestHandler(ResourceHandlerRegistry<IResourceHandler> resourceHandlerRegistry, IResourceRequestHandler resourceRequestHandler) { + this.logic = new ResourceRequestHandlerLogic<IRequest, IResponse, IResourceHandler>(CefRequestAdapter.Instance, CefResponseAdapter.Instance, resourceHandlerRegistry, resourceRequestHandler); + } + + protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) { + return logic.OnBeforeResourceLoad(request, callback) ? CefReturnValue.Continue : CefReturnValue.Cancel; + } + + protected override IResourceHandler GetResourceHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request) { + return logic.GetResourceHandler(request); + } + + protected override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) { + var filter = logic.GetResourceResponseFilter(request, response); + return filter == null ? null : new CefResponseFilter(filter); + } + + protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength) { + logic.OnResourceLoadComplete(request); + } + } +} diff --git a/Browser/Base/CefResourceRequestHandlerFactory.cs b/Browser/Base/CefResourceRequestHandlerFactory.cs new file mode 100644 index 00000000..c211d7d8 --- /dev/null +++ b/Browser/Base/CefResourceRequestHandlerFactory.cs @@ -0,0 +1,22 @@ +using System.Diagnostics.CodeAnalysis; +using CefSharp; +using TweetLib.Browser.CEF.Data; +using TweetLib.Browser.CEF.Logic; +using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler; + +namespace TweetDuck.Browser.Base { + sealed class CefResourceRequestHandlerFactory : IResourceRequestHandlerFactory { + bool IResourceRequestHandlerFactory.HasHandlers => true; + + private readonly ResourceRequestHandlerFactoryLogic<CefResourceRequestHandler, IResourceHandler, IRequest> logic; + + public CefResourceRequestHandlerFactory(IResourceRequestHandler resourceRequestHandler, ResourceHandlerRegistry<IResourceHandler> registry) { + this.logic = new ResourceRequestHandlerFactoryLogic<CefResourceRequestHandler, IResourceHandler, IRequest>(CefRequestAdapter.Instance, new CefResourceRequestHandler(registry, resourceRequestHandler), registry); + } + + [SuppressMessage("ReSharper", "RedundantAssignment")] + CefSharp.IResourceRequestHandler IResourceRequestHandlerFactory.GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) { + return logic.GetResourceRequestHandler(request, ref disableDefaultHandling); + } + } +} diff --git a/Browser/Base/CefResponseAdapter.cs b/Browser/Base/CefResponseAdapter.cs new file mode 100644 index 00000000..1a5ca59b --- /dev/null +++ b/Browser/Base/CefResponseAdapter.cs @@ -0,0 +1,31 @@ +using CefSharp; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetDuck.Browser.Base { + sealed class CefResponseAdapter : IResponseAdapter<IResponse> { + public static CefResponseAdapter Instance { get; } = new CefResponseAdapter(); + + private CefResponseAdapter() {} + + public void SetCharset(IResponse response, string charset) { + response.Charset = charset; + } + + public void SetMimeType(IResponse response, string mimeType) { + response.MimeType = mimeType; + } + + public void SetStatus(IResponse response, int statusCode, string statusText) { + response.StatusCode = statusCode; + response.StatusText = statusText; + } + + public void SetHeader(IResponse response, string header, string value) { + response.SetHeaderByName(header, value, overwrite: true); + } + + public string GetHeader(IResponse response, string header) { + return response.Headers[header]; + } + } +} diff --git a/Browser/Base/CefResponseFilter.cs b/Browser/Base/CefResponseFilter.cs new file mode 100644 index 00000000..ad2fb042 --- /dev/null +++ b/Browser/Base/CefResponseFilter.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; +using CefSharp; +using TweetLib.Browser.CEF.Logic; + +namespace TweetDuck.Browser.Base { + sealed class CefResponseFilter : IResponseFilter { + private readonly ResponseFilterLogic logic; + + public CefResponseFilter(ResponseFilterLogic logic) { + this.logic = logic; + } + + bool IResponseFilter.InitFilter() { + return true; + } + + FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten) { + return logic.Filter(dataIn, out dataInRead, dataOut, dataOut.Length, out dataOutWritten) switch { + ResponseFilterLogic.FilterStatus.NeedMoreData => FilterStatus.NeedMoreData, + ResponseFilterLogic.FilterStatus.Done => FilterStatus.Done, + _ => FilterStatus.Error + }; + } + + void IDisposable.Dispose() {} + } +} diff --git a/Browser/Adapters/CefSchemeHandlerFactory.cs b/Browser/Base/CefSchemeHandlerFactory.cs similarity index 55% rename from Browser/Adapters/CefSchemeHandlerFactory.cs rename to Browser/Base/CefSchemeHandlerFactory.cs index 12356e1e..322fe6e8 100644 --- a/Browser/Adapters/CefSchemeHandlerFactory.cs +++ b/Browser/Base/CefSchemeHandlerFactory.cs @@ -1,9 +1,9 @@ -using System; using CefSharp; using CefSharp.WinForms; +using TweetLib.Browser.CEF.Logic; using TweetLib.Browser.Interfaces; -namespace TweetDuck.Browser.Adapters { +namespace TweetDuck.Browser.Base { sealed class CefSchemeHandlerFactory : ISchemeHandlerFactory { public static void Register(CefSettings settings, ICustomSchemeHandler handler) { settings.RegisterScheme(new CefCustomScheme { @@ -16,14 +16,14 @@ public static void Register(CefSettings settings, ICustomSchemeHandler handler) }); } - private readonly ICustomSchemeHandler handler; + private readonly SchemeHandlerFactoryLogic<IRequest, IResourceHandler> logic; private CefSchemeHandlerFactory(ICustomSchemeHandler handler) { - this.handler = handler; + this.logic = new SchemeHandlerFactoryLogic<IRequest, IResourceHandler>(handler, CefRequestAdapter.Instance, CefResourceHandlerFactory.Instance); } - public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) { - return Uri.TryCreate(request.Url, UriKind.Absolute, out var uri) ? handler.Resolve(uri)?.Visit(CefSchemeResourceVisitor.Instance) : null; + IResourceHandler ISchemeHandlerFactory.Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) { + return logic.Create(request); } } } diff --git a/Browser/FormBrowser.cs b/Browser/FormBrowser.cs index 28f67dd6..f0106b38 100644 --- a/Browser/FormBrowser.cs +++ b/Browser/FormBrowser.cs @@ -47,13 +47,10 @@ public bool IsWaiting { public UpdateInstaller UpdateInstaller { get; private set; } - #pragma warning disable IDE0069 // Disposable fields should be disposed - private readonly TweetDeckBrowser browser; - private readonly FormNotificationTweet notification; - #pragma warning restore IDE0069 // Disposable fields should be disposed - private readonly ResourceCache resourceCache; + private readonly TweetDeckBrowser browser; private readonly ITweetDeckInterface tweetDeckInterface; + private readonly FormNotificationTweet notification; private readonly PluginManager plugins; private readonly UpdateChecker updates; private readonly ContextMenu contextMenu; diff --git a/Browser/Handling/ContextMenuBase.cs b/Browser/Handling/ContextMenuBase.cs index 05c5d7e5..99cc7edd 100644 --- a/Browser/Handling/ContextMenuBase.cs +++ b/Browser/Handling/ContextMenuBase.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using System.Drawing; using CefSharp; -using TweetDuck.Browser.Adapters; +using TweetDuck.Browser.Base; using TweetDuck.Configuration; using TweetDuck.Utils; +using TweetLib.Browser.CEF.Data; using TweetLib.Browser.Contexts; namespace TweetDuck.Browser.Handling { - abstract class ContextMenuBase : IContextMenuHandler { + class ContextMenuBase : IContextMenuHandler { private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand) 26500; private static readonly HashSet<CefMenuCommand> AllowedCefCommands = new HashSet<CefMenuCommand> { @@ -31,11 +32,17 @@ abstract class ContextMenuBase : IContextMenuHandler { protected static UserConfig Config => Program.Config.User; private readonly TweetLib.Browser.Interfaces.IContextMenuHandler handler; - private readonly CefContextMenuActionRegistry actionRegistry; + private readonly ContextMenuActionRegistry actionRegistry; - protected ContextMenuBase(TweetLib.Browser.Interfaces.IContextMenuHandler handler) { + public ContextMenuBase(TweetLib.Browser.Interfaces.IContextMenuHandler handler) { this.handler = handler; - this.actionRegistry = new CefContextMenuActionRegistry(); + this.actionRegistry = new ContextMenuActionRegistry(); + } + + private sealed class ContextMenuActionRegistry : ContextMenuActionRegistry<CefMenuCommand> { + protected override CefMenuCommand NextId(int n) { + return CefMenuCommand.UserFirst + 500 + n; + } } protected virtual Context CreateContext(IContextMenuParams parameters) { @@ -62,8 +69,13 @@ public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser bro } AddSeparator(model); - handler.Show(new CefContextMenuModel(model, actionRegistry), CreateContext(parameters)); + handler?.Show(new CefContextMenuModel(model, actionRegistry), CreateContext(parameters)); RemoveSeparatorIfLast(model); + AddLastContextMenuItems(model); + } + + protected virtual void AddLastContextMenuItems(IMenuModel model) { + AddDebugMenuItems(model); } public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) { @@ -94,16 +106,16 @@ protected static void AddDebugMenuItems(IMenuModel model) { } } - protected static void RemoveSeparatorIfLast(IMenuModel model) { - if (model.Count > 0 && model.GetTypeAt(model.Count - 1) == MenuItemType.Separator) { - model.RemoveAt(model.Count - 1); - } - } - protected static void AddSeparator(IMenuModel model) { if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator) { // do not add separators if there is nothing to separate model.AddSeparator(); } } + + private static void RemoveSeparatorIfLast(IMenuModel model) { + if (model.Count > 0 && model.GetTypeAt(model.Count - 1) == MenuItemType.Separator) { + model.RemoveAt(model.Count - 1); + } + } } } diff --git a/Browser/Handling/ContextMenuBrowser.cs b/Browser/Handling/ContextMenuBrowser.cs index c7fad0fa..5b59d4c6 100644 --- a/Browser/Handling/ContextMenuBrowser.cs +++ b/Browser/Handling/ContextMenuBrowser.cs @@ -1,6 +1,6 @@ using System.Windows.Forms; using CefSharp; -using TweetDuck.Browser.Adapters; +using TweetDuck.Browser.Base; using TweetDuck.Controls; using TweetLib.Browser.Contexts; using TweetLib.Core.Features.TweetDeck; @@ -62,6 +62,8 @@ public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser br } } + protected override void AddLastContextMenuItems(IMenuModel model) {} + public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) { if (base.OnContextMenuCommand(browserControl, browser, frame, parameters, commandId, eventFlags)) { return true; diff --git a/Browser/Handling/ContextMenuGuide.cs b/Browser/Handling/ContextMenuGuide.cs deleted file mode 100644 index cde67b5c..00000000 --- a/Browser/Handling/ContextMenuGuide.cs +++ /dev/null @@ -1,13 +0,0 @@ -using CefSharp; -using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler; - -namespace TweetDuck.Browser.Handling { - sealed class ContextMenuGuide : ContextMenuBase { - public ContextMenuGuide(IContextMenuHandler handler) : base(handler) {} - - public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) { - base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model); - AddDebugMenuItems(model); - } - } -} diff --git a/Browser/Handling/ContextMenuNotification.cs b/Browser/Handling/ContextMenuNotification.cs index 4be8edae..9ab9a155 100644 --- a/Browser/Handling/ContextMenuNotification.cs +++ b/Browser/Handling/ContextMenuNotification.cs @@ -20,7 +20,6 @@ protected override Context CreateContext(IContextMenuParams parameters) { public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) { base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model); - AddDebugMenuItems(model); form.InvokeAsyncSafe(() => form.ContextMenuOpen = true); } diff --git a/Browser/Handling/JavaScriptDialogHandler.cs b/Browser/Handling/CustomJsDialogHandler.cs similarity index 98% rename from Browser/Handling/JavaScriptDialogHandler.cs rename to Browser/Handling/CustomJsDialogHandler.cs index e7f3baed..c6fd38d9 100644 --- a/Browser/Handling/JavaScriptDialogHandler.cs +++ b/Browser/Handling/CustomJsDialogHandler.cs @@ -7,7 +7,7 @@ using TweetDuck.Utils; namespace TweetDuck.Browser.Handling { - sealed class JavaScriptDialogHandler : IJsDialogHandler { + sealed class CustomJsDialogHandler : IJsDialogHandler { private static FormMessage CreateMessageForm(string caption, string text) { MessageBoxIcon icon = MessageBoxIcon.None; int pipe = text.IndexOf('|'); diff --git a/Browser/Handling/CustomLifeSpanHandler.cs b/Browser/Handling/CustomLifeSpanHandler.cs deleted file mode 100644 index 2b65410c..00000000 --- a/Browser/Handling/CustomLifeSpanHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using CefSharp; -using CefSharp.Handler; -using TweetLib.Core; -using TweetLib.Core.Features.Twitter; - -namespace TweetDuck.Browser.Handling { - sealed class CustomLifeSpanHandler : LifeSpanHandler { - public static bool HandleLinkClick(WindowOpenDisposition targetDisposition, string targetUrl) { - switch (targetDisposition) { - case WindowOpenDisposition.NewBackgroundTab: - case WindowOpenDisposition.NewForegroundTab: - case WindowOpenDisposition.NewPopup when !TwitterUrls.IsAllowedPopupUrl(targetUrl): - case WindowOpenDisposition.NewWindow: - App.SystemHandler.OpenBrowser(targetUrl); - return true; - - default: - return false; - } - } - - protected override bool OnBeforePopup(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures, IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser) { - newBrowser = null; - return HandleLinkClick(targetDisposition, targetUrl); - } - - protected override bool DoClose(IWebBrowser browserControl, IBrowser browser) { - return false; - } - } -} diff --git a/Browser/Handling/DownloadRequestClient.cs b/Browser/Handling/DownloadRequestClient.cs deleted file mode 100644 index 09d4062d..00000000 --- a/Browser/Handling/DownloadRequestClient.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.IO; -using CefSharp; - -namespace TweetDuck.Browser.Handling { - sealed class DownloadRequestClient : UrlRequestClient { - private readonly FileStream fileStream; - private readonly Action onSuccess; - private readonly Action<Exception> onError; - - private bool hasFailed; - - public DownloadRequestClient(FileStream fileStream, Action onSuccess, Action<Exception> onError) { - this.fileStream = fileStream; - this.onSuccess = onSuccess; - this.onError = onError; - } - - protected override bool GetAuthCredentials(bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) { - onError?.Invoke(new Exception("This URL requires authentication.")); - fileStream.Dispose(); - hasFailed = true; - return false; - } - - protected override void OnDownloadData(IUrlRequest request, Stream data) { - if (hasFailed) { - return; - } - - try { - data.CopyTo(fileStream); - } catch (Exception e) { - fileStream.Dispose(); - onError?.Invoke(e); - hasFailed = true; - } - } - - protected override void OnRequestComplete(IUrlRequest request) { - if (hasFailed) { - return; - } - - bool isEmpty = fileStream.Position == 0; - fileStream.Dispose(); - - var status = request.RequestStatus; - if (status == UrlRequestStatus.Failed) { - onError?.Invoke(new Exception("Unknown error.")); - } - else if (status == UrlRequestStatus.Success) { - if (isEmpty) { - onError?.Invoke(new Exception("File is empty.")); - return; - } - - onSuccess?.Invoke(); - } - } - } -} diff --git a/Browser/Handling/DragHandlerBrowser.cs b/Browser/Handling/DragHandlerBrowser.cs deleted file mode 100644 index 85518f87..00000000 --- a/Browser/Handling/DragHandlerBrowser.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using CefSharp; -using CefSharp.Enums; - -namespace TweetDuck.Browser.Handling { - sealed class DragHandlerBrowser : IDragHandler { - private readonly RequestHandlerBrowser requestHandler; - - public DragHandlerBrowser(RequestHandlerBrowser requestHandler) { - this.requestHandler = requestHandler; - } - - public bool OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask) { - void TriggerDragStart(string type, string data = null) { - browserControl.BrowserCore.ExecuteScriptAsync("window.TDGF_onGlobalDragStart", type, data); - } - - requestHandler.BlockNextUserNavUrl = dragData.LinkUrl; // empty if not a link - - if (dragData.IsLink) { - TriggerDragStart("link", dragData.LinkUrl); - } - else if (dragData.IsFragment) { - TriggerDragStart("text", dragData.FragmentText.Trim()); - } - else { - TriggerDragStart("unknown"); - } - - return false; - } - - public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IFrame frame, IList<DraggableRegion> regions) {} - } -} diff --git a/Browser/Handling/PopupHandler.cs b/Browser/Handling/PopupHandler.cs new file mode 100644 index 00000000..1871d072 --- /dev/null +++ b/Browser/Handling/PopupHandler.cs @@ -0,0 +1,19 @@ +using TweetLib.Browser.CEF.Interfaces; +using TweetLib.Core; +using TweetLib.Core.Features.Twitter; + +namespace TweetDuck.Browser.Handling { + sealed class PopupHandler : IPopupHandler { + public static PopupHandler Instance { get; } = new PopupHandler(); + + private PopupHandler() {} + + public bool IsPopupAllowed(string url) { + return TwitterUrls.IsAllowedPopupUrl(url); + } + + public void OpenExternalBrowser(string url) { + App.SystemHandler.OpenBrowser(url); + } + } +} diff --git a/Browser/Handling/RequestHandlerBase.cs b/Browser/Handling/RequestHandlerBase.cs deleted file mode 100644 index acd06833..00000000 --- a/Browser/Handling/RequestHandlerBase.cs +++ /dev/null @@ -1,22 +0,0 @@ -using CefSharp; -using CefSharp.Handler; - -namespace TweetDuck.Browser.Handling { - class RequestHandlerBase : RequestHandler { - private readonly bool autoReload; - - public RequestHandlerBase(bool autoReload) { - this.autoReload = autoReload; - } - - protected override bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture) { - return CustomLifeSpanHandler.HandleLinkClick(targetDisposition, targetUrl); - } - - protected override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status) { - if (autoReload) { - browser.Reload(); - } - } - } -} diff --git a/Browser/Handling/RequestHandlerBrowser.cs b/Browser/Handling/RequestHandlerBrowser.cs deleted file mode 100644 index 96c3e787..00000000 --- a/Browser/Handling/RequestHandlerBrowser.cs +++ /dev/null @@ -1,23 +0,0 @@ -using CefSharp; -using TweetLib.Core.Features.Twitter; - -namespace TweetDuck.Browser.Handling { - sealed class RequestHandlerBrowser : RequestHandlerBase { - public string BlockNextUserNavUrl { get; set; } - - public RequestHandlerBrowser() : base(true) {} - - protected override bool OnBeforeBrowse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect) { - if (userGesture && request.TransitionType == TransitionType.LinkClicked) { - bool block = request.Url == BlockNextUserNavUrl; - BlockNextUserNavUrl = string.Empty; - return block; - } - else if (request.TransitionType.HasFlag(TransitionType.ForwardBack) && TwitterUrls.IsTweetDeck(frame.Url)) { - return true; - } - - return base.OnBeforeBrowse(browserControl, browser, frame, request, userGesture, isRedirect); - } - } -} diff --git a/Browser/Handling/ResourceHandlerNotification.cs b/Browser/Handling/ResourceHandlerNotification.cs deleted file mode 100644 index 4c3cfedf..00000000 --- a/Browser/Handling/ResourceHandlerNotification.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Specialized; -using System.IO; -using System.Text; -using CefSharp; -using CefSharp.Callback; - -namespace TweetDuck.Browser.Handling { - sealed class ResourceHandlerNotification : IResourceHandler { - private readonly NameValueCollection headers = new NameValueCollection(0); - private MemoryStream dataIn; - - public void SetHTML(string html) { - dataIn?.Dispose(); - dataIn = ResourceHandler.GetMemoryStream(html, Encoding.UTF8); - } - - public void Dispose() { - if (dataIn != null) { - dataIn.Dispose(); - dataIn = null; - } - } - - bool IResourceHandler.Open(IRequest request, out bool handleRequest, ICallback callback) { - callback.Dispose(); - handleRequest = true; - - if (dataIn != null) { - dataIn.Position = 0; - } - - return true; - } - - void IResourceHandler.GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl) { - redirectUrl = null; - - response.MimeType = "text/html"; - response.StatusCode = 200; - response.StatusText = "OK"; - response.Headers = headers; - responseLength = dataIn?.Length ?? 0; - } - - bool IResourceHandler.Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback) { - callback?.Dispose(); // TODO unnecessary null check once ReadResponse is removed - - try { - byte[] buffer = new byte[Math.Min(dataIn.Length - dataIn.Position, dataOut.Length)]; - int length = buffer.Length; - - dataIn.Read(buffer, 0, length); - dataOut.Write(buffer, 0, length); - bytesRead = length; - } catch { // catch IOException, possibly NullReferenceException if dataIn is null - bytesRead = 0; - } - - return bytesRead > 0; - } - - bool IResourceHandler.Skip(long bytesToSkip, out long bytesSkipped, IResourceSkipCallback callback) { - bytesSkipped = -2; // ERR_FAILED - callback.Dispose(); - return false; - } - - bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback) { - return ((IResourceHandler) this).Open(request, out bool _, callback); - } - - bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback) { - return ((IResourceHandler) this).Read(dataOut, out bytesRead, null); - } - - void IResourceHandler.Cancel() {} - } -} diff --git a/Browser/Notification/FormNotificationBase.cs b/Browser/Notification/FormNotificationBase.cs index 199db797..913dc51a 100644 --- a/Browser/Notification/FormNotificationBase.cs +++ b/Browser/Notification/FormNotificationBase.cs @@ -1,11 +1,13 @@ using System.Drawing; +using System.Text; using System.Windows.Forms; using CefSharp.WinForms; -using TweetDuck.Browser.Adapters; +using TweetDuck.Browser.Base; using TweetDuck.Browser.Handling; using TweetDuck.Configuration; using TweetDuck.Controls; using TweetDuck.Utils; +using TweetLib.Browser.CEF.Data; using TweetLib.Browser.Interfaces; using TweetLib.Core.Features.Notifications; using TweetLib.Core.Features.Twitter; @@ -88,14 +90,11 @@ protected virtual FormBorderStyle NotificationBorderStyle { private readonly FormBrowser owner; - protected readonly IBrowserComponent browserComponent; + protected readonly ChromiumWebBrowser browser; + protected readonly CefBrowserComponent browserComponent; private readonly NotificationBrowser browserImpl; - #pragma warning disable IDE0069 // Disposable fields should be disposed - protected readonly ChromiumWebBrowser browser; - #pragma warning restore IDE0069 // Disposable fields should be disposed - - private readonly ResourceHandlerNotification resourceHandler = new ResourceHandlerNotification(); + private readonly CefByteArrayResourceHandler resourceHandler = new CefByteArrayResourceHandler(); private DesktopNotification currentNotification; private int pauseCounter; @@ -115,11 +114,10 @@ protected FormNotificationBase(FormBrowser owner, CreateBrowserImplFunc createBr this.owner = owner; this.owner.FormClosed += owner_FormClosed; - this.browser = new ChromiumWebBrowser(NotificationBrowser.BlankURL) { - RequestHandler = new RequestHandlerBase(false) - }; - - this.browserComponent = new ComponentImpl(browser, this); + this.browser = new ChromiumWebBrowser(NotificationBrowser.BlankURL); + this.browserComponent = new CefBrowserComponent(browser, handler => new ContextMenuNotification(this, handler), autoReload: false); + this.browserComponent.ResourceHandlerRegistry.RegisterStatic(NotificationBrowser.BlankURL, string.Empty); + this.browserComponent.ResourceHandlerRegistry.RegisterDynamic(TwitterUrls.TweetDeck, resourceHandler); this.browserImpl = createBrowserImpl(this, browserComponent); this.browser.Dock = DockStyle.None; @@ -139,29 +137,9 @@ protected FormNotificationBase(FormBrowser owner, CreateBrowserImplFunc createBr UpdateTitle(); } - protected sealed class ComponentImpl : CefBrowserComponent { - private readonly FormNotificationBase owner; - - public ComponentImpl(ChromiumWebBrowser browser, FormNotificationBase owner) : base(browser) { - this.owner = owner; - } - - protected override ContextMenuBase SetupContextMenu(IContextMenuHandler handler) { - return new ContextMenuNotification(owner, handler); - } - - protected override CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler) { - var registry = new CefResourceHandlerRegistry(); - registry.RegisterStatic(NotificationBrowser.BlankURL, string.Empty); - registry.RegisterDynamic(TwitterUrls.TweetDeck, owner.resourceHandler); - return new CefResourceHandlerFactory(handler, registry); - } - } - protected override void Dispose(bool disposing) { if (disposing) { components?.Dispose(); - resourceHandler.Dispose(); } base.Dispose(disposing); @@ -207,7 +185,7 @@ public virtual void ResumeNotification() { protected virtual void LoadTweet(DesktopNotification tweet) { currentNotification = tweet; - resourceHandler.SetHTML(browserImpl.GetTweetHTML(tweet)); + resourceHandler.SetResource(new ByteArrayResource(browserImpl.GetTweetHTML(tweet), Encoding.UTF8)); browser.Load(TwitterUrls.TweetDeck); DisplayTooltip(null); diff --git a/Browser/Notification/SoundNotification.cs b/Browser/Notification/SoundNotification.cs index 10b69a7a..cbbb79c5 100644 --- a/Browser/Notification/SoundNotification.cs +++ b/Browser/Notification/SoundNotification.cs @@ -1,20 +1,21 @@ using System.Drawing; using System.IO; using System.Windows.Forms; -using TweetDuck.Browser.Adapters; +using CefSharp; using TweetDuck.Controls; using TweetDuck.Dialogs; using TweetDuck.Dialogs.Settings; using TweetDuck.Management; +using TweetLib.Browser.CEF.Data; using TweetLib.Core.Features.TweetDeck; namespace TweetDuck.Browser.Notification { sealed class SoundNotification : ISoundNotificationHandler { public const string SupportedFormats = "*.wav;*.ogg;*.mp3;*.flac;*.opus;*.weba;*.webm"; - private readonly CefResourceHandlerRegistry registry; + private readonly ResourceHandlerRegistry<IResourceHandler> registry; - public SoundNotification(CefResourceHandlerRegistry registry) { + public SoundNotification(ResourceHandlerRegistry<IResourceHandler> registry) { this.registry = registry; } diff --git a/Browser/TweetDeckBrowser.cs b/Browser/TweetDeckBrowser.cs index 69b84ae4..08957132 100644 --- a/Browser/TweetDeckBrowser.cs +++ b/Browser/TweetDeckBrowser.cs @@ -2,7 +2,7 @@ using System.Drawing; using CefSharp; using CefSharp.WinForms; -using TweetDuck.Browser.Adapters; +using TweetDuck.Browser.Base; using TweetDuck.Browser.Handling; using TweetDuck.Browser.Notification; using TweetDuck.Configuration; @@ -11,8 +11,6 @@ using TweetLib.Core.Features.TweetDeck; using TweetLib.Core.Features.Twitter; using TweetLib.Core.Systems.Updates; -using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler; -using IResourceRequestHandler = TweetLib.Browser.Interfaces.IResourceRequestHandler; using TweetDeckBrowserImpl = TweetLib.Core.Features.TweetDeck.TweetDeckBrowser; namespace TweetDuck.Browser { @@ -42,24 +40,18 @@ public bool IsTweetDeckWebsite { private readonly ChromiumWebBrowser browser; public TweetDeckBrowser(FormBrowser owner, PluginManager pluginManager, ITweetDeckInterface tweetDeckInterface, UpdateChecker updateChecker) { - RequestHandlerBrowser requestHandler = new RequestHandlerBrowser(); - this.browser = new ChromiumWebBrowser(TwitterUrls.TweetDeck) { DialogHandler = new FileDialogHandler(), - DragHandler = new DragHandlerBrowser(requestHandler), - KeyboardHandler = new CustomKeyboardHandler(owner), - RequestHandler = requestHandler + KeyboardHandler = new CustomKeyboardHandler(owner) }; // ReSharper disable once PossiblyImpureMethodCallOnReadonlyVariable this.browser.BrowserSettings.BackgroundColor = (uint) TweetDeckBrowserImpl.BackgroundColor.ToArgb(); var extraContext = new TweetDeckExtraContext(); - var resourceHandlerRegistry = new CefResourceHandlerRegistry(); - var soundNotificationHandler = new SoundNotification(resourceHandlerRegistry); - this.browserComponent = new ComponentImpl(browser, owner, extraContext, resourceHandlerRegistry); - this.browserImpl = new TweetDeckBrowserImpl(browserComponent, tweetDeckInterface, extraContext, soundNotificationHandler, pluginManager, updateChecker); + this.browserComponent = new CefBrowserComponent(browser, handler => new ContextMenuBrowser(owner, handler, extraContext)); + this.browserImpl = new TweetDeckBrowserImpl(browserComponent, tweetDeckInterface, extraContext, new SoundNotification(browserComponent.ResourceHandlerRegistry), pluginManager, updateChecker); if (Arguments.HasFlag(Arguments.ArgIgnoreGDPR)) { browserComponent.PageLoadEnd += (sender, args) => { @@ -72,26 +64,6 @@ public TweetDeckBrowser(FormBrowser owner, PluginManager pluginManager, ITweetDe owner.Controls.Add(browser); } - private sealed class ComponentImpl : CefBrowserComponent { - private readonly FormBrowser owner; - private readonly TweetDeckExtraContext extraContext; - private readonly CefResourceHandlerRegistry registry; - - public ComponentImpl(ChromiumWebBrowser browser, FormBrowser owner, TweetDeckExtraContext extraContext, CefResourceHandlerRegistry registry) : base(browser) { - this.owner = owner; - this.extraContext = extraContext; - this.registry = registry; - } - - protected override ContextMenuBase SetupContextMenu(IContextMenuHandler handler) { - return new ContextMenuBrowser(owner, handler, extraContext); - } - - protected override CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler) { - return new CefResourceHandlerFactory(handler, registry); - } - } - public void PrepareSize(Size size) { if (!Ready) { browser.Size = size; diff --git a/Dialogs/FormGuide.cs b/Dialogs/FormGuide.cs index 6aba986c..94ffb731 100644 --- a/Dialogs/FormGuide.cs +++ b/Dialogs/FormGuide.cs @@ -2,12 +2,11 @@ using System.Windows.Forms; using CefSharp.WinForms; using TweetDuck.Browser; -using TweetDuck.Browser.Adapters; +using TweetDuck.Browser.Base; using TweetDuck.Browser.Handling; using TweetDuck.Controls; using TweetDuck.Management; using TweetDuck.Utils; -using TweetLib.Browser.Interfaces; using TweetLib.Core.Features; namespace TweetDuck.Dialogs { @@ -31,9 +30,7 @@ public static void Show(string hash = null) { } } - #pragma warning disable IDE0069 // Disposable fields should be disposed private readonly ChromiumWebBrowser browser; - #pragma warning restore IDE0069 // Disposable fields should be disposed private FormGuide(string url, Form owner) { InitializeComponent(); @@ -43,13 +40,12 @@ private FormGuide(string url, Form owner) { VisibleChanged += (sender, args) => this.MoveToCenter(owner); browser = new ChromiumWebBrowser(url) { - KeyboardHandler = new CustomKeyboardHandler(null), - RequestHandler = new RequestHandlerBase(true) + KeyboardHandler = new CustomKeyboardHandler(null) }; browser.BrowserSettings.BackgroundColor = (uint) BackColor.ToArgb(); - var browserComponent = new ComponentImpl(browser); + var browserComponent = new CefBrowserComponent(browser); var browserImpl = new BaseBrowser(browserComponent); BrowserUtils.SetupDockOnLoad(browserComponent, browser); @@ -62,18 +58,6 @@ private FormGuide(string url, Form owner) { }; } - private sealed class ComponentImpl : CefBrowserComponent { - public ComponentImpl(ChromiumWebBrowser browser) : base(browser) {} - - protected override ContextMenuBase SetupContextMenu(IContextMenuHandler handler) { - return new ContextMenuGuide(handler); - } - - protected override CefResourceHandlerFactory SetupResourceHandlerFactory(IResourceRequestHandler handler) { - return new CefResourceHandlerFactory(handler, null); - } - } - protected override void Dispose(bool disposing) { if (disposing) { components?.Dispose(); diff --git a/Dialogs/Settings/DialogSettingsCefArgs.cs b/Dialogs/Settings/DialogSettingsCefArgs.cs index 5b781cc2..987f6142 100644 --- a/Dialogs/Settings/DialogSettingsCefArgs.cs +++ b/Dialogs/Settings/DialogSettingsCefArgs.cs @@ -1,8 +1,8 @@ using System; using System.Windows.Forms; using TweetDuck.Controls; +using TweetLib.Browser.CEF.Utils; using TweetLib.Core; -using TweetLib.Core.Features.Chromium; namespace TweetDuck.Dialogs.Settings { sealed partial class DialogSettingsCefArgs : Form { diff --git a/Management/BrowserCache.cs b/Management/BrowserCache.cs index 018f0bed..f71d9531 100644 --- a/Management/BrowserCache.cs +++ b/Management/BrowserCache.cs @@ -3,11 +3,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using TweetLib.Browser.CEF.Utils; using TweetLib.Core; namespace TweetDuck.Management { static class BrowserCache { - public static string CacheFolder => Path.Combine(App.StoragePath, "Cache"); + public static string CacheFolder => CefUtils.GetCacheFolder(App.StoragePath); private static bool clearOnExit; private static Timer autoClearTimer; diff --git a/Program.cs b/Program.cs index baecdef1..5199888e 100644 --- a/Program.cs +++ b/Program.cs @@ -5,16 +5,16 @@ using CefSharp.WinForms; using TweetDuck.Application; using TweetDuck.Browser; -using TweetDuck.Browser.Adapters; +using TweetDuck.Browser.Base; using TweetDuck.Browser.Handling; using TweetDuck.Configuration; using TweetDuck.Dialogs; using TweetDuck.Management; using TweetDuck.Updates; using TweetDuck.Utils; +using TweetLib.Browser.CEF.Utils; using TweetLib.Core; using TweetLib.Core.Application; -using TweetLib.Core.Features.Chromium; using TweetLib.Core.Features.Plugins; using TweetLib.Core.Features.Plugins.Config; using TweetLib.Core.Features.TweetDeck; diff --git a/TweetDuck.csproj b/TweetDuck.csproj index 6a5fa797..6314ecb0 100644 --- a/TweetDuck.csproj +++ b/TweetDuck.csproj @@ -66,6 +66,10 @@ <Project>{eefb1f37-7cad-46bd-8042-66e7b502ab02}</Project> <Name>TweetLib.Browser</Name> </ProjectReference> + <ProjectReference Include="lib\TweetLib.Browser.CEF\TweetLib.Browser.CEF.csproj"> + <Project>{1b7793c6-9002-483e-9bd7-897fe6cd18fb}</Project> + <Name>TweetLib.Browser.CEF</Name> + </ProjectReference> <ProjectReference Include="lib\TweetLib.Core\TweetLib.Core.csproj"> <Project>{93ba3cb4-a812-4949-b07d-8d393fb38937}</Project> <Name>TweetLib.Core</Name> @@ -91,29 +95,32 @@ <Compile Include="Application\FileDialogs.cs" /> <Compile Include="Application\MessageDialogs.cs" /> <Compile Include="Application\SystemHandler.cs" /> - <Compile Include="Browser\Adapters\CefBrowserComponent.cs" /> - <Compile Include="Browser\Adapters\CefContextMenuActionRegistry.cs" /> - <Compile Include="Browser\Adapters\CefContextMenuModel.cs" /> - <Compile Include="Browser\Adapters\CefResourceHandlerFactory.cs" /> - <Compile Include="Browser\Adapters\CefResourceHandlerRegistry.cs" /> - <Compile Include="Browser\Adapters\CefResourceRequestHandler.cs" /> - <Compile Include="Browser\Adapters\CefSchemeHandlerFactory.cs" /> - <Compile Include="Browser\Adapters\CefSchemeResourceVisitor.cs" /> + <Compile Include="Browser\Base\CefBrowserAdapter.cs" /> + <Compile Include="Browser\Base\CefBrowserComponent.cs" /> + <Compile Include="Browser\Base\CefContextMenuModel.cs" /> + <Compile Include="Browser\Base\CefDownloadRequestClient.cs" /> + <Compile Include="Browser\Base\CefDragDataAdapter.cs" /> + <Compile Include="Browser\Base\CefDragHandler.cs" /> + <Compile Include="Browser\Base\CefErrorCodeAdapter.cs" /> + <Compile Include="Browser\Base\CefFrameAdapter.cs" /> + <Compile Include="Browser\Base\CefLifeSpanHandler.cs" /> + <Compile Include="Browser\Base\CefRequestAdapter.cs" /> + <Compile Include="Browser\Base\CefRequestHandler.cs" /> + <Compile Include="Browser\Base\CefResourceHandlerFactory.cs" /> + <Compile Include="Browser\Base\CefByteArrayResourceHandler.cs" /> + <Compile Include="Browser\Base\CefResourceRequestHandlerFactory.cs" /> + <Compile Include="Browser\Base\CefResourceRequestHandler.cs" /> + <Compile Include="Browser\Base\CefResponseAdapter.cs" /> + <Compile Include="Browser\Base\CefResponseFilter.cs" /> + <Compile Include="Browser\Base\CefSchemeHandlerFactory.cs" /> <Compile Include="Browser\Handling\BrowserProcessHandler.cs" /> <Compile Include="Browser\Handling\ContextMenuBase.cs" /> <Compile Include="Browser\Handling\ContextMenuBrowser.cs" /> - <Compile Include="Browser\Handling\ContextMenuGuide.cs" /> <Compile Include="Browser\Handling\ContextMenuNotification.cs" /> <Compile Include="Browser\Handling\CustomKeyboardHandler.cs" /> - <Compile Include="Browser\Handling\CustomLifeSpanHandler.cs" /> - <Compile Include="Browser\Handling\DownloadRequestClient.cs" /> - <Compile Include="Browser\Handling\DragHandlerBrowser.cs" /> <Compile Include="Browser\Handling\FileDialogHandler.cs" /> - <Compile Include="Browser\Handling\JavaScriptDialogHandler.cs" /> - <Compile Include="Browser\Handling\RequestHandlerBase.cs" /> - <Compile Include="Browser\Handling\RequestHandlerBrowser.cs" /> - <Compile Include="Browser\Handling\ResourceHandlerNotification.cs" /> - <Compile Include="Browser\Handling\ResponseFilter.cs" /> + <Compile Include="Browser\Handling\CustomJsDialogHandler.cs" /> + <Compile Include="Browser\Handling\PopupHandler.cs" /> <Compile Include="Browser\Notification\FormNotificationExample.cs"> <SubType>Form</SubType> </Compile> diff --git a/TweetDuck.sln b/TweetDuck.sln index f2a4cde9..1aaf9849 100644 --- a/TweetDuck.sln +++ b/TweetDuck.sln @@ -14,10 +14,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetL EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Browser", "lib\TweetLib.Browser\TweetLib.Browser.csproj", "{EEFB1F37-7CAD-46BD-8042-66E7B502AB02}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Browser.CEF", "lib\TweetLib.Browser.CEF\TweetLib.Browser.CEF.csproj", "{1B7793C6-9002-483E-9BD7-897FE6CD18FB}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Utils", "lib\TweetLib.Utils\TweetLib.Utils.csproj", "{476B1007-B12C-447F-B855-9886048201D6}" EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Core", "lib\TweetTest.Core\TweetTest.Core.fsproj", "{2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Browser.CEF", "lib\TweetTest.Browser.CEF\TweetTest.Browser.CEF.fsproj", "{651B77C2-3745-4DAA-982C-398C2856E038}" +EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Utils", "lib\TweetTest.Utils\TweetTest.Utils.fsproj", "{07F6D350-B16F-44E2-804D-C1142E1E345F}" EndProject Global @@ -50,6 +54,10 @@ Global {EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Debug|x86.Build.0 = Debug|x86 {EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Release|x86.ActiveCfg = Release|x86 {EEFB1F37-7CAD-46BD-8042-66E7B502AB02}.Release|x86.Build.0 = Release|x86 + {1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Debug|x86.ActiveCfg = Debug|x86 + {1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Debug|x86.Build.0 = Debug|x86 + {1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Release|x86.ActiveCfg = Release|x86 + {1B7793C6-9002-483E-9BD7-897FE6CD18FB}.Release|x86.Build.0 = Release|x86 {476B1007-B12C-447F-B855-9886048201D6}.Debug|x86.ActiveCfg = Debug|x86 {476B1007-B12C-447F-B855-9886048201D6}.Debug|x86.Build.0 = Debug|x86 {476B1007-B12C-447F-B855-9886048201D6}.Release|x86.ActiveCfg = Release|x86 @@ -58,6 +66,10 @@ Global {2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Debug|x86.Build.0 = Debug|x86 {2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Release|x86.ActiveCfg = Release|x86 {2D7DFBA6-057D-4111-B61B-E4CB1E2BF369}.Release|x86.Build.0 = Release|x86 + {651B77C2-3745-4DAA-982C-398C2856E038}.Debug|x86.ActiveCfg = Debug|x86 + {651B77C2-3745-4DAA-982C-398C2856E038}.Debug|x86.Build.0 = Debug|x86 + {651B77C2-3745-4DAA-982C-398C2856E038}.Release|x86.ActiveCfg = Release|x86 + {651B77C2-3745-4DAA-982C-398C2856E038}.Release|x86.Build.0 = Release|x86 {07F6D350-B16F-44E2-804D-C1142E1E345F}.Debug|x86.ActiveCfg = Debug|x86 {07F6D350-B16F-44E2-804D-C1142E1E345F}.Debug|x86.Build.0 = Debug|x86 {07F6D350-B16F-44E2-804D-C1142E1E345F}.Release|x86.ActiveCfg = Release|x86 diff --git a/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs b/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs new file mode 100644 index 00000000..3680e12b --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs @@ -0,0 +1,86 @@ +using System; +using TweetLib.Browser.Base; +using TweetLib.Browser.CEF.Interfaces; +using TweetLib.Browser.Events; +using TweetLib.Browser.Interfaces; +using TweetLib.Utils.Static; + +namespace TweetLib.Browser.CEF.Component { + public abstract class BrowserComponent<TFrame> : IBrowserComponent where TFrame : IDisposable { + public bool Ready { get; private set; } + + public string Url => browser.Url; + public abstract string CacheFolder { get; } + + public event EventHandler<BrowserLoadedEventArgs>? BrowserLoaded; + public event EventHandler<PageLoadEventArgs>? PageLoadStart; + public event EventHandler<PageLoadEventArgs>? PageLoadEnd; + + private readonly IBrowserWrapper<TFrame> browser; + private readonly IFrameAdapter<TFrame> frameAdapter; + + protected BrowserComponent(IBrowserWrapper<TFrame> browser, IFrameAdapter<TFrame> frameAdapter) { + this.browser = browser; + this.frameAdapter = frameAdapter; + } + + public abstract void Setup(BrowserSetup setup); + public abstract void AttachBridgeObject(string name, object bridge); + public abstract void DownloadFile(string url, string path, Action? onSuccess, Action<Exception>? onError); + + private sealed class BrowserLoadedEventArgsImpl : BrowserLoadedEventArgs { + private readonly IBrowserWrapper<TFrame> browser; + + public BrowserLoadedEventArgsImpl(IBrowserWrapper<TFrame> browser) { + this.browser = browser; + } + + public override void AddDictionaryWords(params string[] words) { + foreach (string word in words) { + browser.AddWordToDictionary(word); + } + } + } + + protected void OnLoadingStateChanged(bool isLoading) { + if (!isLoading && !Ready) { + Ready = true; + BrowserLoaded?.Invoke(this, new BrowserLoadedEventArgsImpl(browser)); + BrowserLoaded = null; + } + } + + protected void OnLoadError<T>(string failedUrl, T errorCode, IErrorCodeAdapter<T> errorCodeAdapter) { + if (errorCodeAdapter.IsAborted(errorCode)) { + return; + } + + if (!failedUrl.StartsWithOrdinal("td://resources/error/")) { + using TFrame frame = browser.MainFrame; + + if (frameAdapter.IsValid(frame)) { + string? errorName = errorCodeAdapter.GetName(errorCode); + string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty); + frameAdapter.LoadUrl(frame, "td://resources/error/error.html#" + Uri.EscapeDataString(errorTitle)); + } + } + } + + protected void OnFrameLoadStart(string url, TFrame frame) { + if (frameAdapter.IsMain(frame)) { + PageLoadStart?.Invoke(this, new PageLoadEventArgs(url)); + } + } + + protected void OnFrameLoadEnd(string url, TFrame frame) { + if (frameAdapter.IsMain(frame)) { + PageLoadEnd?.Invoke(this, new PageLoadEventArgs(url)); + } + } + + public void RunScript(string identifier, string script) { + using TFrame frame = browser.MainFrame; + frameAdapter.ExecuteJavaScriptAsync(frame, script, identifier, 1); + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Data/ByteArrayResource.cs b/lib/TweetLib.Browser.CEF/Data/ByteArrayResource.cs new file mode 100644 index 00000000..fce0f4c7 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Data/ByteArrayResource.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Text; + +namespace TweetLib.Browser.CEF.Data { + public sealed class ByteArrayResource { + private const string DefaultMimeType = "text/html"; + private const HttpStatusCode DefaultStatusCode = HttpStatusCode.OK; + private const string DefaultStatusText = "OK"; + + internal byte[] Contents { get; } + internal int Length { get; } + internal string MimeType { get; } + internal HttpStatusCode StatusCode { get; } + internal string StatusText { get; } + + public ByteArrayResource(byte[] contents, string mimeType = DefaultMimeType, HttpStatusCode statusCode = DefaultStatusCode, string statusText = DefaultStatusText) { + this.Contents = contents; + this.Length = contents.Length; + this.MimeType = mimeType; + this.StatusCode = statusCode; + this.StatusText = statusText; + } + + public ByteArrayResource(string contents, Encoding encoding, string mimeType = DefaultMimeType, HttpStatusCode statusCode = DefaultStatusCode, string statusText = DefaultStatusText) : this(encoding.GetBytes(contents), mimeType, statusCode, statusText) {} + } +} diff --git a/lib/TweetLib.Browser.CEF/Data/ContextMenuActionRegistry.cs b/lib/TweetLib.Browser.CEF/Data/ContextMenuActionRegistry.cs new file mode 100644 index 00000000..d310fb2b --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Data/ContextMenuActionRegistry.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace TweetLib.Browser.CEF.Data { + public abstract class ContextMenuActionRegistry<T> { + private readonly Dictionary<T, Action> actions = new (); + + protected abstract T NextId(int n); + + public T AddAction(Action action) { + T id = NextId(actions.Count); + actions[id] = action; + return id; + } + + public bool Execute(T id) { + if (actions.TryGetValue(id, out var action)) { + action(); + return true; + } + + return false; + } + + public void Clear() { + actions.Clear(); + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Data/ResourceHandlerRegistry.cs b/lib/TweetLib.Browser.CEF/Data/ResourceHandlerRegistry.cs new file mode 100644 index 00000000..d761d367 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Data/ResourceHandlerRegistry.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using System.Text; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetLib.Browser.CEF.Data { + public sealed class ResourceHandlerRegistry<TResourceHandler> where TResourceHandler : class { + private readonly IResourceHandlerFactory<TResourceHandler> factory; + private readonly ConcurrentDictionary<string, Func<TResourceHandler>> resourceHandlers = new (StringComparer.OrdinalIgnoreCase); + + public ResourceHandlerRegistry(IResourceHandlerFactory<TResourceHandler> factory) { + this.factory = factory; + } + + internal bool HasHandler(string url) { + return resourceHandlers.ContainsKey(url); + } + + internal TResourceHandler? GetHandler(string url) { + return resourceHandlers.TryGetValue(url, out var handler) ? handler() : null; + } + + private void Register(string url, Func<TResourceHandler> factory) { + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) { + throw new ArgumentException("Resource handler URL must be absolute!"); + } + + resourceHandlers.AddOrUpdate(uri.AbsoluteUri, factory, (_, _) => factory); + } + + public void RegisterStatic(string url, byte[] staticData, string mimeType = "text/html") { + Register(url, () => factory.CreateResourceHandler(new ByteArrayResource(staticData, mimeType))); + } + + public void RegisterStatic(string url, string staticData, string mimeType = "text/html") { + Register(url, () => factory.CreateResourceHandler(new ByteArrayResource(staticData, Encoding.UTF8, mimeType))); + } + + public void RegisterDynamic(string url, TResourceHandler handler) { + Register(url, () => handler); + } + + public void Unregister(string url) { + resourceHandlers.TryRemove(url, out _); + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs b/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs new file mode 100644 index 00000000..6bbdf2c1 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs @@ -0,0 +1,10 @@ +using System; + +namespace TweetLib.Browser.CEF.Interfaces { + public interface IBrowserWrapper<TFrame> where TFrame : IDisposable { + string Url { get; } + TFrame MainFrame { get; } + + void AddWordToDictionary(string word); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IDragDataAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IDragDataAdapter.cs new file mode 100644 index 00000000..100d7abf --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IDragDataAdapter.cs @@ -0,0 +1,9 @@ +namespace TweetLib.Browser.CEF.Interfaces { + public interface IDragDataAdapter<T> { + bool IsLink(T data); + string GetLink(T data); + + bool IsFragment(T data); + string GetFragmentAsText(T data); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IErrorCodeAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IErrorCodeAdapter.cs new file mode 100644 index 00000000..5aec8b40 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IErrorCodeAdapter.cs @@ -0,0 +1,6 @@ +namespace TweetLib.Browser.CEF.Interfaces { + public interface IErrorCodeAdapter<T> { + bool IsAborted(T errorCode); + string? GetName(T errorCode); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IFrameAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IFrameAdapter.cs new file mode 100644 index 00000000..a93a093e --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IFrameAdapter.cs @@ -0,0 +1,8 @@ +namespace TweetLib.Browser.CEF.Interfaces { + public interface IFrameAdapter<T> { + bool IsValid(T frame); + bool IsMain(T frame); + void LoadUrl(T frame, string url); + void ExecuteJavaScriptAsync(T frame, string script, string identifier, int startLine = 1); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IPopupHandler.cs b/lib/TweetLib.Browser.CEF/Interfaces/IPopupHandler.cs new file mode 100644 index 00000000..b40b7738 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IPopupHandler.cs @@ -0,0 +1,6 @@ +namespace TweetLib.Browser.CEF.Interfaces { + public interface IPopupHandler { + bool IsPopupAllowed(string url); + void OpenExternalBrowser(string url); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs new file mode 100644 index 00000000..88393ad7 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs @@ -0,0 +1,19 @@ +using TweetLib.Browser.Request; + +namespace TweetLib.Browser.CEF.Interfaces { + public interface IRequestAdapter<T> { + ulong GetIdentifier(T request); + + string GetUrl(T request); + + void SetUrl(T request, string url); + + bool IsTransitionForwardBack(T request); + + bool IsCspReport(T request); + + ResourceType GetResourceType(T request); + + void SetHeader(T request, string header, string value); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IResourceHandlerFactory.cs b/lib/TweetLib.Browser.CEF/Interfaces/IResourceHandlerFactory.cs new file mode 100644 index 00000000..e13e7a2c --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IResourceHandlerFactory.cs @@ -0,0 +1,8 @@ +using TweetLib.Browser.CEF.Data; + +namespace TweetLib.Browser.CEF.Interfaces { + public interface IResourceHandlerFactory<T> { + T CreateResourceHandler(ByteArrayResource resource); + string GetMimeTypeFromExtension(string extension); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IResponseAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IResponseAdapter.cs new file mode 100644 index 00000000..e1fc1377 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IResponseAdapter.cs @@ -0,0 +1,9 @@ +namespace TweetLib.Browser.CEF.Interfaces { + public interface IResponseAdapter<T> { + void SetCharset(T response, string charset); + void SetMimeType(T response, string mimeType); + void SetStatus(T response, int statusCode, string statusText); + void SetHeader(T response, string header, string value); + string? GetHeader(T response, string header); + } +} diff --git a/lib/TweetLib.Browser.CEF/Lib.cs b/lib/TweetLib.Browser.CEF/Lib.cs new file mode 100644 index 00000000..fde9f468 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Lib.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyTitle("TweetDuck Browser CEF Library")] +[assembly: AssemblyDescription("TweetDuck Browser CEF Library")] +[assembly: AssemblyProduct("TweetLib.Browser.CEF")] diff --git a/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs new file mode 100644 index 00000000..268d3e02 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs @@ -0,0 +1,61 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using TweetLib.Browser.CEF.Data; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetLib.Browser.CEF.Logic { + [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] + public sealed class ByteArrayResourceHandlerLogic<TResponse> { + public delegate void WriteToOut<T>(T dataOut, byte[] dataIn, int position, int length); + + private readonly ByteArrayResource resource; + private readonly IResponseAdapter<TResponse> responseAdapter; + + private int position; + + public ByteArrayResourceHandlerLogic(ByteArrayResource resource, IResponseAdapter<TResponse> responseAdapter) { + this.resource = resource; + this.responseAdapter = responseAdapter; + } + + public bool Open(out bool handleRequest) { + position = 0; + handleRequest = true; + return true; + } + + public void GetResponseHeaders(TResponse response, out long responseLength, out string? redirectUrl) { + responseLength = resource.Length; + redirectUrl = null; + + responseAdapter.SetMimeType(response, resource.MimeType); + responseAdapter.SetStatus(response, (int) resource.StatusCode, resource.StatusText); + responseAdapter.SetCharset(response, "utf-8"); + responseAdapter.SetHeader(response, "Access-Control-Allow-Origin", "*"); + } + + public bool Skip(long bytesToSkip, out long bytesSkipped, IDisposable callback) { + callback.Dispose(); + + position = (int) (position + bytesToSkip); + bytesSkipped = bytesToSkip; + return true; + } + + public bool Read<T>(WriteToOut<T> write, T dataOut, int bytesToRead, out int bytesRead, IDisposable callback) { + callback.Dispose(); + + if (bytesToRead > 0) { + write(dataOut, resource.Contents, position, bytesToRead); + position += bytesToRead; + } + + bytesRead = bytesToRead; + return bytesRead > 0; + } + + public bool Read<T>(WriteToOut<T> write, T dataOut, out int bytesRead, IDisposable callback) { + return Read(write, dataOut, resource.Length - position, out bytesRead, callback); + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs b/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs new file mode 100644 index 00000000..0e9cef28 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs @@ -0,0 +1,73 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +namespace TweetLib.Browser.CEF.Logic { + public sealed class DownloadRequestClientLogic { + public enum RequestStatus { + Unknown, + Success, + Failed + } + + private readonly FileStream fileStream; + private readonly Action? onSuccess; + private readonly Action<Exception>? onError; + + private bool hasFailed; + + public DownloadRequestClientLogic(FileStream fileStream, Action? onSuccess, Action<Exception>? onError) { + this.fileStream = fileStream; + this.onSuccess = onSuccess; + this.onError = onError; + } + + public bool GetAuthCredentials(IDisposable callback) { + callback.Dispose(); + + hasFailed = true; + fileStream.Dispose(); + onError?.Invoke(new Exception("This URL requires authentication.")); + + return false; + } + + public void OnDownloadData(Stream data) { + if (hasFailed) { + return; + } + + try { + data.CopyTo(fileStream); + } catch (Exception e) { + fileStream.Dispose(); + onError?.Invoke(e); + hasFailed = true; + } + } + + [SuppressMessage("ReSharper", "SwitchStatementMissingSomeEnumCasesNoDefault")] + public void OnRequestComplete(RequestStatus status) { + if (hasFailed) { + return; + } + + bool isEmpty = fileStream.Position == 0; + fileStream.Dispose(); + + switch (status) { + case RequestStatus.Failed: + onError?.Invoke(new Exception("Unknown error.")); + break; + + case RequestStatus.Success when isEmpty: + onError?.Invoke(new Exception("File is empty.")); + return; + + case RequestStatus.Success: + onSuccess?.Invoke(); + break; + } + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Logic/DragHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/DragHandlerLogic.cs new file mode 100644 index 00000000..aff9c3f3 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/DragHandlerLogic.cs @@ -0,0 +1,37 @@ +using TweetLib.Browser.CEF.Interfaces; +using TweetLib.Browser.Interfaces; + +namespace TweetLib.Browser.CEF.Logic { + public sealed class DragHandlerLogic<TDragData, TRequest> { + private readonly IScriptExecutor executor; + private readonly RequestHandlerLogic<TRequest> requestHandlerLogic; + private readonly IDragDataAdapter<TDragData> dragDataAdapter; + + public DragHandlerLogic(IScriptExecutor executor, RequestHandlerLogic<TRequest> requestHandlerLogic, IDragDataAdapter<TDragData> dragDataAdapter) { + this.executor = executor; + this.requestHandlerLogic = requestHandlerLogic; + this.dragDataAdapter = dragDataAdapter; + } + + private void TriggerDragStart(string type, string? data = null) { + executor.RunFunction("window.TDGF_onGlobalDragStart && window.TDGF_onGlobalDragStart", type, data); + } + + public bool OnDragEnter(TDragData dragData) { + var link = dragDataAdapter.GetLink(dragData); + requestHandlerLogic.BlockNextUserNavUrl = link; + + if (dragDataAdapter.IsLink(dragData)) { + TriggerDragStart("link", link); + } + else if (dragDataAdapter.IsFragment(dragData)) { + TriggerDragStart("text", dragDataAdapter.GetFragmentAsText(dragData).Trim()); + } + else { + TriggerDragStart("unknown"); + } + + return false; + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Logic/LifeSpanHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/LifeSpanHandlerLogic.cs new file mode 100644 index 00000000..43ee0cb6 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/LifeSpanHandlerLogic.cs @@ -0,0 +1,37 @@ +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetLib.Browser.CEF.Logic { + public sealed class LifeSpanHandlerLogic { + public enum TargetDisposition { + NewBackgroundTab, + NewForegroundTab, + NewPopup, + NewWindow, + Other + } + + private readonly IPopupHandler popupHandler; + + public LifeSpanHandlerLogic(IPopupHandler popupHandler) { + this.popupHandler = popupHandler; + } + + public bool OnBeforePopup(string targetUrl, TargetDisposition targetDisposition) { + switch (targetDisposition) { + case TargetDisposition.NewBackgroundTab: + case TargetDisposition.NewForegroundTab: + case TargetDisposition.NewPopup when !popupHandler.IsPopupAllowed(targetUrl): + case TargetDisposition.NewWindow: + popupHandler.OpenExternalBrowser(targetUrl); + return true; + + default: + return false; + } + } + + public bool DoClose() { + return false; + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Logic/RequestHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/RequestHandlerLogic.cs new file mode 100644 index 00000000..87d408fa --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/RequestHandlerLogic.cs @@ -0,0 +1,29 @@ +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetLib.Browser.CEF.Logic { + public sealed class RequestHandlerLogic<TRequest> { + internal string BlockNextUserNavUrl { get; set; } = string.Empty; + + private readonly IRequestAdapter<TRequest> requestAdapter; + private readonly LifeSpanHandlerLogic lifeSpanHandlerLogic; + + public RequestHandlerLogic(IRequestAdapter<TRequest> requestAdapter, LifeSpanHandlerLogic lifeSpanHandlerLogic) { + this.requestAdapter = requestAdapter; + this.lifeSpanHandlerLogic = lifeSpanHandlerLogic; + } + + private bool ShouldBlockNav(string url) { + bool block = url == BlockNextUserNavUrl; + BlockNextUserNavUrl = string.Empty; + return block; + } + + public bool OnBeforeBrowse(TRequest request, bool userGesture) { + return requestAdapter.IsTransitionForwardBack(request) || (userGesture && ShouldBlockNav(requestAdapter.GetUrl(request))); + } + + public bool OnOpenUrlFromTab(string targetUrl, bool userGesture, LifeSpanHandlerLogic.TargetDisposition targetDisposition) { + return (userGesture && ShouldBlockNav(targetUrl)) || lifeSpanHandlerLogic.OnBeforePopup(targetUrl, targetDisposition); + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerFactoryLogic.cs b/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerFactoryLogic.cs new file mode 100644 index 00000000..ef62a806 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerFactoryLogic.cs @@ -0,0 +1,23 @@ +using System.Diagnostics.CodeAnalysis; +using TweetLib.Browser.CEF.Data; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetLib.Browser.CEF.Logic { + public sealed class ResourceRequestHandlerFactoryLogic<TResourceRequestHandler, TResourceHandler, TRequest> where TResourceHandler : class { + private readonly IRequestAdapter<TRequest> requestAdapter; + private readonly TResourceRequestHandler handler; + private readonly ResourceHandlerRegistry<TResourceHandler> registry; + + public ResourceRequestHandlerFactoryLogic(IRequestAdapter<TRequest> requestAdapter, TResourceRequestHandler handler, ResourceHandlerRegistry<TResourceHandler> registry) { + this.handler = handler; + this.registry = registry; + this.requestAdapter = requestAdapter; + } + + [SuppressMessage("ReSharper", "RedundantAssignment")] + public TResourceRequestHandler GetResourceRequestHandler(TRequest request, ref bool disableDefaultHandling) { + disableDefaultHandling = registry.HasHandler(requestAdapter.GetUrl(request)); + return handler; + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerLogic.cs new file mode 100644 index 00000000..7c1bb524 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/ResourceRequestHandlerLogic.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using TweetLib.Browser.CEF.Data; +using TweetLib.Browser.CEF.Interfaces; +using TweetLib.Browser.Interfaces; +using TweetLib.Browser.Request; + +namespace TweetLib.Browser.CEF.Logic { + public sealed class ResourceRequestHandlerLogic<TRequest, TResponse, TResourceHandler> where TResourceHandler : class { + private readonly IRequestAdapter<TRequest> requestAdapter; + private readonly IResponseAdapter<TResponse> responseAdapter; + private readonly ResourceHandlerRegistry<TResourceHandler> resourceHandlerRegistry; + private readonly IResourceRequestHandler? resourceRequestHandler; + + private readonly Dictionary<ulong, IResponseProcessor> responseProcessors = new (); + + public ResourceRequestHandlerLogic(IRequestAdapter<TRequest> requestAdapter, IResponseAdapter<TResponse> responseAdapter, ResourceHandlerRegistry<TResourceHandler> resourceHandlerRegistry, IResourceRequestHandler? resourceRequestHandler) { + this.requestAdapter = requestAdapter; + this.responseAdapter = responseAdapter; + this.resourceHandlerRegistry = resourceHandlerRegistry; + this.resourceRequestHandler = resourceRequestHandler; + } + + public bool OnBeforeResourceLoad(TRequest request, IDisposable callback) { + if (requestAdapter.IsCspReport(request)) { + callback.Dispose(); + return false; + } + + if (resourceRequestHandler != null) { + var result = resourceRequestHandler.Handle(requestAdapter.GetUrl(request), requestAdapter.GetResourceType(request)); + + switch (result) { + case RequestHandleResult.Redirect redirect: + requestAdapter.SetUrl(request, redirect.Url); + break; + + case RequestHandleResult.Process process: + requestAdapter.SetHeader(request, "Accept-Encoding", "identity"); + responseProcessors[requestAdapter.GetIdentifier(request)] = process.Processor; + break; + + case RequestHandleResult.Cancel: + callback.Dispose(); + return false; + } + } + + return true; + } + + public TResourceHandler? GetResourceHandler(TRequest request) { + return resourceHandlerRegistry.GetHandler(requestAdapter.GetUrl(request)); + } + + public ResponseFilterLogic? GetResourceResponseFilter(TRequest request, TResponse response) { + if (responseProcessors.TryGetValue(requestAdapter.GetIdentifier(request), out var processor) && int.TryParse(responseAdapter.GetHeader(response, "Content-Length"), out int totalBytes)) { + return new ResponseFilterLogic(processor, totalBytes); + } + + return null; + } + + public void OnResourceLoadComplete(TRequest request) { + responseProcessors.Remove(requestAdapter.GetIdentifier(request)); + } + } +} diff --git a/Browser/Handling/ResponseFilter.cs b/lib/TweetLib.Browser.CEF/Logic/ResponseFilterLogic.cs similarity index 71% rename from Browser/Handling/ResponseFilter.cs rename to lib/TweetLib.Browser.CEF/Logic/ResponseFilterLogic.cs index 351d3285..0f075a41 100644 --- a/Browser/Handling/ResponseFilter.cs +++ b/lib/TweetLib.Browser.CEF/Logic/ResponseFilterLogic.cs @@ -1,10 +1,14 @@ using System; using System.IO; -using CefSharp; using TweetLib.Browser.Interfaces; -namespace TweetDuck.Browser.Handling { - sealed class ResponseFilter : IResponseFilter { +namespace TweetLib.Browser.CEF.Logic { + public sealed class ResponseFilterLogic { + public enum FilterStatus { + NeedMoreData, + Done + } + private enum State { Reading, Writing, @@ -17,26 +21,21 @@ private enum State { private State state; private int offset; - public ResponseFilter(IResponseProcessor processor, int totalBytes) { + internal ResponseFilterLogic(IResponseProcessor processor, int totalBytes) { this.processor = processor; this.responseData = new byte[totalBytes]; this.state = State.Reading; } - public bool InitFilter() { - return true; - } - - FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten) { + public FilterStatus Filter(Stream? dataIn, out long dataInRead, Stream dataOut, long dataOutLength, out long dataOutWritten) { int responseLength = responseData.Length; if (state == State.Reading) { int bytesToRead = Math.Min(responseLength - offset, (int) Math.Min(dataIn?.Length ?? 0, int.MaxValue)); + int bytesRead = dataIn?.Read(responseData, offset, bytesToRead) ?? 0; - dataIn?.Read(responseData, offset, bytesToRead); - offset += bytesToRead; - - dataInRead = bytesToRead; + offset += bytesRead; + dataInRead = bytesRead; dataOutWritten = 0; if (offset >= responseLength) { @@ -48,7 +47,7 @@ FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream d return FilterStatus.NeedMoreData; } else if (state == State.Writing) { - int bytesToWrite = Math.Min(responseLength - offset, (int) Math.Min(dataOut.Length, int.MaxValue)); + int bytesToWrite = Math.Min(responseLength - offset, (int) Math.Min(dataOutLength, int.MaxValue)); if (bytesToWrite > 0) { dataOut.Write(responseData, offset, bytesToWrite); @@ -70,7 +69,5 @@ FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream d throw new InvalidOperationException("This resource filter cannot be reused."); } } - - public void Dispose() {} } } diff --git a/lib/TweetLib.Browser.CEF/Logic/SchemeHandlerFactoryLogic.cs b/lib/TweetLib.Browser.CEF/Logic/SchemeHandlerFactoryLogic.cs new file mode 100644 index 00000000..fccd7e7a --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/SchemeHandlerFactoryLogic.cs @@ -0,0 +1,21 @@ +using System; +using TweetLib.Browser.CEF.Interfaces; +using TweetLib.Browser.Interfaces; + +namespace TweetLib.Browser.CEF.Logic { + public sealed class SchemeHandlerFactoryLogic<TRequest, TResourceHandler> where TResourceHandler : class { + private readonly ICustomSchemeHandler handler; + private readonly IRequestAdapter<TRequest> requestAdapter; + private readonly ISchemeResourceVisitor<TResourceHandler> resourceVisitor; + + public SchemeHandlerFactoryLogic(ICustomSchemeHandler handler, IRequestAdapter<TRequest> requestAdapter, IResourceHandlerFactory<TResourceHandler> resourceHandlerFactory) { + this.handler = handler; + this.requestAdapter = requestAdapter; + this.resourceVisitor = new SchemeResourceVisitor<TResourceHandler>(resourceHandlerFactory); + } + + public TResourceHandler? Create(TRequest request) { + return Uri.TryCreate(requestAdapter.GetUrl(request), UriKind.Absolute, out var uri) ? handler.Resolve(uri)?.Visit(resourceVisitor) : null; + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Logic/SchemeResourceVisitor.cs b/lib/TweetLib.Browser.CEF/Logic/SchemeResourceVisitor.cs new file mode 100644 index 00000000..4ed41243 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/SchemeResourceVisitor.cs @@ -0,0 +1,29 @@ +using System; +using System.Net; +using TweetLib.Browser.CEF.Data; +using TweetLib.Browser.CEF.Interfaces; +using TweetLib.Browser.Interfaces; +using TweetLib.Browser.Request; + +namespace TweetLib.Browser.CEF.Logic { + internal abstract class SchemeResourceVisitor { + protected static readonly SchemeResource.Status FileIsEmpty = new (HttpStatusCode.NoContent, "File is empty."); + } + + internal sealed class SchemeResourceVisitor<TResourceHandler> : SchemeResourceVisitor, ISchemeResourceVisitor<TResourceHandler> { + private readonly IResourceHandlerFactory<TResourceHandler> factory; + + public SchemeResourceVisitor(IResourceHandlerFactory<TResourceHandler> factory) { + this.factory = factory; + } + + public TResourceHandler Status(SchemeResource.Status status) { + return factory.CreateResourceHandler(new ByteArrayResource(Array.Empty<byte>(), statusCode: status.Code, statusText: status.Message)); + } + + public TResourceHandler File(SchemeResource.File file) { + byte[] contents = file.Contents; + return contents.Length == 0 ? Status(FileIsEmpty) : factory.CreateResourceHandler(new ByteArrayResource(contents, factory.GetMimeTypeFromExtension(file.Extension))); + } + } +} diff --git a/lib/TweetLib.Browser.CEF/TweetLib.Browser.CEF.csproj b/lib/TweetLib.Browser.CEF/TweetLib.Browser.CEF.csproj new file mode 100644 index 00000000..0c7d1bc3 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/TweetLib.Browser.CEF.csproj @@ -0,0 +1,20 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <Platforms>x86</Platforms> + <LangVersion>9</LangVersion> + <Nullable>enable</Nullable> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + </PropertyGroup> + + <ItemGroup> + <Compile Include="..\..\Version.cs" Link="Version.cs" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\TweetLib.Browser\TweetLib.Browser.csproj" /> + <ProjectReference Include="..\TweetLib.Utils\TweetLib.Utils.csproj" /> + </ItemGroup> + +</Project> diff --git a/lib/TweetLib.Core/Features/Chromium/CefUtils.cs b/lib/TweetLib.Browser.CEF/Utils/CefUtils.cs similarity index 83% rename from lib/TweetLib.Core/Features/Chromium/CefUtils.cs rename to lib/TweetLib.Browser.CEF/Utils/CefUtils.cs index 66c3a54f..e2c7f14f 100644 --- a/lib/TweetLib.Core/Features/Chromium/CefUtils.cs +++ b/lib/TweetLib.Browser.CEF/Utils/CefUtils.cs @@ -1,8 +1,13 @@ +using System.IO; using System.Text.RegularExpressions; using TweetLib.Utils.Collections; -namespace TweetLib.Core.Features.Chromium { +namespace TweetLib.Browser.CEF.Utils { public static class CefUtils { + public static string GetCacheFolder(string storagePath) { + return Path.Combine(storagePath, "Cache"); + } + public static CommandLineArgs ParseCommandLineArguments(string argumentString) { CommandLineArgs args = new CommandLineArgs(); diff --git a/lib/TweetTest.Browser.CEF/TweetTest.Browser.CEF.fsproj b/lib/TweetTest.Browser.CEF/TweetTest.Browser.CEF.fsproj new file mode 100644 index 00000000..2ebd9ce2 --- /dev/null +++ b/lib/TweetTest.Browser.CEF/TweetTest.Browser.CEF.fsproj @@ -0,0 +1,28 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net472</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="coverlet.collector" Version="3.1.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\TweetLib.Browser.CEF\TweetLib.Browser.CEF.csproj" /> + </ItemGroup> + + <ItemGroup> + <Compile Include="Utils\TestCefUtils.fs" /> + </ItemGroup> + +</Project> diff --git a/lib/TweetTest.Core/Features/Chromium/TestCefUtils.fs b/lib/TweetTest.Browser.CEF/Utils/TestCefUtils.fs similarity index 94% rename from lib/TweetTest.Core/Features/Chromium/TestCefUtils.fs rename to lib/TweetTest.Browser.CEF/Utils/TestCefUtils.fs index 4fe99e7e..4721bcb7 100644 --- a/lib/TweetTest.Core/Features/Chromium/TestCefUtils.fs +++ b/lib/TweetTest.Browser.CEF/Utils/TestCefUtils.fs @@ -1,6 +1,6 @@ -namespace TweetTest.Core.Features.Chromium.CefUtils +namespace TweetTest.Browser.CEF.Utils.CefUtils -open TweetLib.Core.Features.Chromium +open TweetLib.Browser.CEF.Utils open Xunit diff --git a/lib/TweetTest.Core/TweetTest.Core.fsproj b/lib/TweetTest.Core/TweetTest.Core.fsproj index bba0c85b..1810c097 100644 --- a/lib/TweetTest.Core/TweetTest.Core.fsproj +++ b/lib/TweetTest.Core/TweetTest.Core.fsproj @@ -22,7 +22,6 @@ </ItemGroup> <ItemGroup> - <Compile Include="Features\Chromium\TestCefUtils.fs" /> <Compile Include="Features\Twitter\TestTwitterUrls.fs" /> </ItemGroup>