Skip to content

Commit

Permalink
Custom html error page implementation (#2136)
Browse files Browse the repository at this point in the history
  • Loading branch information
mallexxx authored Feb 18, 2024
1 parent 03495cb commit ffff49a
Show file tree
Hide file tree
Showing 50 changed files with 1,846 additions and 505 deletions.
2 changes: 2 additions & 0 deletions Configuration/Tests/IntegrationTests.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION DBP
INFOPLIST_FILE = IntegrationTests/Info.plist
PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.Integration-Tests

SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/IntegrationTests/Common/IntegrationTestsBridging.h

TEST_HOST=$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo
100 changes: 67 additions & 33 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Alert-Medium-Multicolor-16.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

This file was deleted.

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Globe-Multicolor-16.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
1 change: 1 addition & 0 deletions DuckDuckGo/Bridging.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#import "WKWebView+Private.h"
#import "NSException+Catch.h"
#import "NSObject+performSelector.h"
#import "WKGeolocationProvider.h"

#ifndef APPSTORE
Expand Down
27 changes: 27 additions & 0 deletions DuckDuckGo/Common/Extensions/NSObject+performSelector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// NSObject+performSelector.h
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (performSelector)
- (id)performSelector:(SEL)selector withArguments:(NSArray *)arguments;
@end

NS_ASSUME_NONNULL_END
52 changes: 52 additions & 0 deletions DuckDuckGo/Common/Extensions/NSObject+performSelector.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// NSObject+performSelector.m
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#import "NSObject+performSelector.h"

@implementation NSObject (performSelector)

- (id)performSelector:(SEL)selector withArguments:(NSArray *)arguments {
NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector];

if (!methodSignature) {
[[[NSException alloc] initWithName:@"InvalidSelectorOrTarget" reason:[NSString stringWithFormat:@"Could not get method signature for selector %@ on %@", NSStringFromSelector(selector), self] userInfo:nil] raise];
}

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setSelector:selector];
[invocation setTarget:self];

for (NSInteger i = 0; i < arguments.count; i++) {
id argument = arguments[i];
[invocation setArgument:&argument atIndex:i + 2]; // Indices 0 and 1 are reserved for target and selector
}

[invocation invoke];

if (methodSignature.methodReturnLength > 0) {
id returnValue;
[invocation getReturnValue:&returnValue];

return returnValue;
}

return nil;
}


@end
39 changes: 38 additions & 1 deletion DuckDuckGo/Common/Extensions/StringExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
// limitations under the License.
//

import Foundation
import BrowserServicesKit
import Common
import Foundation
import UniformTypeIdentifiers

extension String {
Expand All @@ -32,6 +33,42 @@ extension String {
self.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
.replacingOccurrences(of: "'", with: "\\'")
.replacingOccurrences(of: "\n", with: "\\n")
}

private static let unicodeHtmlCharactersMapping: [Character: String] = [
"&": "&amp;",
"\"": "&quot;",
"'": "&apos;",
"<": "&lt;",
">": "&gt;",
"/": "&#x2F;",
"!": "&excl;",
"$": "&#36;",
"%": "&percnt;",
"=": "&#61;",
"#": "&#35;",
"@": "&#64;",
"[": "&#91;",
"\\": "&#92;",
"]": "&#93;",
"^": "&#94;",
"`": "&#97;",
"{": "&#123;",
"}": "&#125;",
]
func escapedUnicodeHtmlString() -> String {
var result = ""

for character in self {
if let mapped = Self.unicodeHtmlCharactersMapping[character] {
result.append(mapped)
} else {
result.append(character)
}
}

return result
}

init(_ staticString: StaticString) {
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/Common/Extensions/URLExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ extension URL {
static let welcome = URL(string: "duck://welcome")!
static let settings = URL(string: "duck://settings")!
static let bookmarks = URL(string: "duck://bookmarks")!
// base url for Error Page Alternate HTML loaded into Web View
static let error = URL(string: "duck://error")!

static let dataBrokerProtection = URL(string: "duck://dbp")!

Expand Down
35 changes: 35 additions & 0 deletions DuckDuckGo/Common/Extensions/WKBackForwardListItemExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// WKBackForwardListItemExtension.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import WebKit

extension WKBackForwardListItem {

// sometimes WKBackForwardListItem returns wrong or outdated title
private static let tabTitleKey = UnsafeRawPointer(bitPattern: "tabTitleKey".hashValue)!
var tabTitle: String? {
get {
objc_getAssociatedObject(self, Self.tabTitleKey) as? String
}
set {
objc_setAssociatedObject(self, Self.tabTitleKey, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}

}
17 changes: 17 additions & 0 deletions DuckDuckGo/Common/Extensions/WKWebViewExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// limitations under the License.
//

import Common
import Navigation
import WebKit

Expand Down Expand Up @@ -231,6 +232,21 @@ extension WKWebView {
self.evaluateJavaScript("window.open(\(urlEnc), '_blank', 'noopener, noreferrer')")
}

func loadAlternateHTML(_ html: String, baseURL: URL, forUnreachableURL failingURL: URL) {
guard responds(to: Selector.loadAlternateHTMLString) else {
if #available(macOS 12.0, *) {
os_log(.error, log: .navigation, "WKWebView._loadAlternateHTMLString not available")
loadSimulatedRequest(URLRequest(url: failingURL), responseHTML: html)
}
return
}
self.perform(Selector.loadAlternateHTMLString, withArguments: [html, baseURL, failingURL])
}

func setDocumentHtml(_ html: String) {
self.evaluateJavaScript("document.open(); document.write('\(html.escapedJavaScriptString())'); document.close()", in: nil, in: .defaultClient)
}

@MainActor
var mimeType: String? {
get async {
Expand Down Expand Up @@ -285,6 +301,7 @@ extension WKWebView {
enum Selector {
static let fullScreenPlaceholderView = NSSelectorFromString("_fullScreenPlaceholderView")
static let printOperationWithPrintInfoForFrame = NSSelectorFromString("_printOperationWithPrintInfo:forFrame:")
static let loadAlternateHTMLString = NSSelectorFromString("_loadAlternateHTMLString:baseURL:forUnreachableURL:")
}

}
8 changes: 6 additions & 2 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//

import Foundation
import Navigation

struct UserText {

Expand Down Expand Up @@ -212,12 +213,15 @@ struct UserText {
static let tabPreferencesTitle = NSLocalizedString("tab.preferences.title", value: "Settings", comment: "Tab preferences title")
static let tabBookmarksTitle = NSLocalizedString("tab.bookmarks.title", value: "Bookmarks", comment: "Tab bookmarks title")
static let tabOnboardingTitle = NSLocalizedString("tab.onboarding.title", value: "Welcome", comment: "Tab onboarding title")
static let tabErrorTitle = NSLocalizedString("tab.error.title", value: "Oops!", comment: "Tab error title")
static let tabErrorTitle = NSLocalizedString("tab.error.title", value: "Failed to open page", comment: "Tab error title")
static let errorPageHeader = NSLocalizedString("page.error.header", value: "DuckDuckGo can’t load this page.", comment: "Error page heading text")
static let webProcessCrashPageHeader = NSLocalizedString("page.crash.header", value: "This webpage has crashed.", comment: "Error page heading text shown when a Web Page process had crashed")
static let webProcessCrashPageMessage = NSLocalizedString("page.crash.message", value: "Try reloading the page or come back later.", comment: "Error page message text shown when a Web Page process had crashed")

static let openSystemPreferences = NSLocalizedString("open.preferences", value: "Open System Preferences", comment: "Open System Preferences (to re-enable permission for the App) (up to and including macOS 12")
static let openSystemSettings = NSLocalizedString("open.settings", value: "Open System Settings…", comment: "")
static let checkForUpdate = NSLocalizedString("check.for.update", value: "Check for Update", comment: "Button users can use to check for a new update")

static let unknownErrorMessage = NSLocalizedString("error.unknown", value: "An unknown error has occurred", comment: "Error page subtitle")
static let unknownErrorTryAgainMessage = NSLocalizedString("error.unknown.try.again", value: "An unknown error has occurred", comment: "Generic error message on a dialog for when the cause is not known.")

static let moveTabToNewWindow = NSLocalizedString("options.menu.move.tab.to.new.window",
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/Common/View/AppKit/ColorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal class ColorView: NSView {
init(frame: NSRect, backgroundColor: NSColor? = nil, cornerRadius: CGFloat = 0, borderColor: NSColor? = nil, borderWidth: CGFloat = 0, interceptClickEvents: Bool = false) {
super.init(frame: frame)

self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = backgroundColor
self.cornerRadius = cornerRadius
self.borderColor = borderColor
Expand Down
8 changes: 1 addition & 7 deletions DuckDuckGo/DataExport/BookmarksExporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct BookmarksExporter {
for entity in entities {
if let bookmark = entity as? Bookmark {
content.append(Template.bookmark(level: level,
title: bookmark.title.escapedForHTML,
title: bookmark.title.escapedUnicodeHtmlString(),
url: bookmark.url,
isFavorite: bookmark.isFavorite))
}
Expand Down Expand Up @@ -100,12 +100,6 @@ extension BookmarksExporter {

fileprivate extension String {

var escapedForHTML: String {
self.replacingOccurrences(of: "&", with: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
}

static func indent(by level: Int) -> String {
return String(repeating: "\t", count: level)
}
Expand Down
45 changes: 45 additions & 0 deletions DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// ErrorPageHTMLTemplate.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import ContentScopeScripts
import WebKit

struct ErrorPageHTMLTemplate {

static var htmlTemplatePath: String {
guard let file = ContentScopeScripts.Bundle.path(forResource: "index", ofType: "html", inDirectory: "pages/errorpage") else {
assertionFailure("HTML template not found")
return ""
}
return file
}

let error: WKError
let header: String

func makeHTMLFromTemplate() -> String {
guard let html = try? String(contentsOfFile: Self.htmlTemplatePath) else {
assertionFailure("Should be able to load template")
return ""
}
return html.replacingOccurrences(of: "$ERROR_DESCRIPTION$", with: error.localizedDescription.escapedUnicodeHtmlString(), options: .literal)
.replacingOccurrences(of: "$HEADER$", with: header.escapedUnicodeHtmlString(), options: .literal)
}

}
Loading

0 comments on commit ffff49a

Please sign in to comment.