Move URL from WebCore to WTF
[WebKit-https.git] / Source / WebKit / UIProcess / Cocoa / WKSafeBrowsingWarning.mm
1 /*
2  * Copyright (C) 2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "WKSafeBrowsingWarning.h"
28
29 #import "PageClient.h"
30 #import "SafeBrowsingWarning.h"
31 #import <WebCore/LocalizedStrings.h>
32 #import <wtf/URL.h>
33 #import <wtf/BlockPtr.h>
34 #import <wtf/Language.h>
35
36 constexpr CGFloat exclamationPointSize = 30;
37 constexpr CGFloat titleSize = 26;
38 constexpr CGFloat boxCornerRadius = 6;
39 #if HAVE(SAFE_BROWSING)
40 constexpr CGFloat marginSize = 20;
41 constexpr CGFloat maxWidth = 675;
42 #endif
43
44 #if PLATFORM(MAC)
45 constexpr CGFloat textSize = 14;
46 using ColorType = NSColor;
47 using FontType = NSFont;
48 using TextViewType = NSTextView;
49 using ButtonType = NSButton;
50 using AlignmentType = NSLayoutAttribute;
51 using ViewType = NSView;
52 using SizeType = NSSize;
53 #else
54 constexpr CGFloat textSize = 20;
55 using ColorType = UIColor;
56 using FontType = UIFont;
57 using TextViewType = UITextView;
58 using ButtonType = UIButton;
59 using AlignmentType = UIStackViewAlignment;
60 using ViewType = UIView;
61 using SizeType = CGSize;
62 #endif
63
64 enum class WarningItem : uint8_t {
65     Background,
66     BoxBackground,
67     ExclamationPoint,
68     TitleText,
69     MessageText,
70     ShowDetailsButton,
71     GoBackButton
72 };
73
74 static ColorType *colorForItem(WarningItem item, ViewType *warning)
75 {
76     ASSERT([warning isKindOfClass:[WKSafeBrowsingWarning class]]);
77 #if PLATFORM(MAC)
78
79     auto colorNamed = [] (NSString *name) -> ColorType* {
80 #if HAVE(SAFE_BROWSING)
81         return [NSColor colorNamed:name bundle:[NSBundle bundleWithIdentifier:@"com.apple.WebKit"]];
82 #else
83         ASSERT_NOT_REACHED();
84         return nil;
85 #endif
86     };
87
88     switch (item) {
89     case WarningItem::Background:
90         return colorNamed(@"WKSafeBrowsingWarningBackground");
91     case WarningItem::BoxBackground:
92         return [NSColor windowBackgroundColor];
93     case WarningItem::TitleText:
94     case WarningItem::ExclamationPoint:
95         return colorNamed(@"WKSafeBrowsingWarningTitle");
96     case WarningItem::MessageText:
97         return colorNamed(@"WKSafeBrowsingWarningText");
98     case WarningItem::ShowDetailsButton:
99     case WarningItem::GoBackButton:
100         ASSERT_NOT_REACHED();
101         return nil;
102     }
103 #else
104     UIColor *red = [UIColor colorWithRed:0.998 green:0.239 blue:0.233 alpha:1.0];
105     UIColor *white = [UIColor whiteColor];
106     bool narrow = warning.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact;
107
108     switch (item) {
109     case WarningItem::Background:
110         return red;
111     case WarningItem::BoxBackground:
112         return narrow ? red : white;
113     case WarningItem::TitleText:
114     case WarningItem::ExclamationPoint:
115         return narrow ? white : red;
116     case WarningItem::MessageText:
117     case WarningItem::ShowDetailsButton:
118         return narrow ? white : [UIColor darkTextColor];
119     case WarningItem::GoBackButton:
120         return narrow ? white : warning.tintColor;
121     }
122 #endif
123     ASSERT_NOT_REACHED();
124     return nil;
125 }
126
127 @interface WKSafeBrowsingExclamationPoint : ViewType
128 @end
129
130 @implementation WKSafeBrowsingExclamationPoint
131
132 - (void)drawRect:(RectType)rect
133 {
134     constexpr CGFloat centerX = exclamationPointSize / 2;
135     constexpr CGFloat pointCenterY = exclamationPointSize * 7 / 30;
136     constexpr CGFloat pointRadius = 2.25 * exclamationPointSize / 30;
137     constexpr CGFloat lineBottomCenterY = exclamationPointSize * 13 / 30;
138     constexpr CGFloat lineTopCenterY = exclamationPointSize * 23 / 30;
139     constexpr CGFloat lineRadius = 1.75 * exclamationPointSize / 30;
140     ViewType *warning = self.superview.superview;
141 #if PLATFORM(MAC)
142     [colorForItem(WarningItem::ExclamationPoint, warning) set];
143     NSBezierPath *exclamationPoint = [NSBezierPath bezierPathWithOvalInRect:NSMakeRect(0, 0, exclamationPointSize, exclamationPointSize)];
144     [exclamationPoint appendBezierPathWithArcWithCenter: { centerX, lineBottomCenterY } radius:lineRadius startAngle:0 endAngle:180 clockwise:YES];
145     [exclamationPoint appendBezierPathWithArcWithCenter: { centerX, lineTopCenterY } radius:lineRadius startAngle:180 endAngle:360 clockwise:YES];
146     [exclamationPoint lineToPoint: { centerX + lineRadius, lineBottomCenterY }];
147     [exclamationPoint appendBezierPathWithArcWithCenter: { centerX, pointCenterY } radius:pointRadius startAngle:0 endAngle:180 clockwise:YES];
148     [exclamationPoint appendBezierPathWithArcWithCenter: { centerX, pointCenterY } radius:pointRadius startAngle:180 endAngle:360 clockwise:YES];
149 #else
150     auto flip = [] (auto y) {
151         return exclamationPointSize - y;
152     };
153     [colorForItem(WarningItem::BoxBackground, warning) set];
154     auto square = CGRectMake(0, 0, exclamationPointSize, exclamationPointSize);
155     [[UIBezierPath bezierPathWithRect:square] fill];
156     
157     [colorForItem(WarningItem::ExclamationPoint, warning) set];
158     UIBezierPath *exclamationPoint = [UIBezierPath bezierPathWithOvalInRect:square];
159     [exclamationPoint addArcWithCenter: { centerX, flip(lineTopCenterY) } radius:lineRadius startAngle:2 * piDouble endAngle:piDouble clockwise:NO];
160     [exclamationPoint addArcWithCenter: { centerX, flip(lineBottomCenterY) } radius:lineRadius startAngle:piDouble endAngle:0 clockwise:NO];
161     [exclamationPoint addArcWithCenter: { centerX, flip(pointCenterY) } radius:pointRadius startAngle:0 endAngle:piDouble clockwise:NO];
162     [exclamationPoint addArcWithCenter: { centerX, flip(pointCenterY) } radius:pointRadius startAngle:piDouble endAngle:piDouble * 2 clockwise:NO];
163     [exclamationPoint addLineToPoint: { centerX + lineRadius, flip(lineBottomCenterY) }];
164     [exclamationPoint addLineToPoint: { centerX + lineRadius, flip(lineTopCenterY) }];
165 #endif
166     [exclamationPoint fill];
167 }
168
169 - (NSSize)intrinsicContentSize
170 {
171     return { exclamationPointSize, exclamationPointSize };
172 }
173
174 @end
175
176 static ButtonType *makeButton(WarningItem item, WKSafeBrowsingWarning *warning, SEL action)
177 {
178     NSString *title = nil;
179     if (item == WarningItem::ShowDetailsButton)
180         title = WEB_UI_NSSTRING(@"Show details", "Action from safe browsing warning");
181     else
182         title = WEB_UI_NSSTRING(@"Go back", "Action from safe browsing warning");
183     title = [title capitalizedString];
184 #if PLATFORM(MAC)
185     return [NSButton buttonWithTitle:title target:warning action:action];
186 #else
187     UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
188     NSAttributedString *attributedTitle = [[[NSAttributedString alloc] initWithString:title attributes:@{
189         NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle),
190         NSUnderlineColorAttributeName:[UIColor whiteColor],
191         NSForegroundColorAttributeName:colorForItem(item, warning),
192         NSFontAttributeName:[FontType systemFontOfSize:textSize]
193     }] autorelease];
194     [button setAttributedTitle:attributedTitle forState:UIControlStateNormal];
195     [button addTarget:warning action:action forControlEvents:UIControlEventTouchUpInside];
196     return button;
197 #endif
198 }
199
200 static ViewType *makeLabel(NSAttributedString *attributedString)
201 {
202 #if PLATFORM(MAC)
203     return [NSTextField labelWithAttributedString:attributedString];
204 #else
205     auto label = [[UILabel new] autorelease];
206     label.attributedText = attributedString;
207     label.lineBreakMode = NSLineBreakByWordWrapping;
208     label.numberOfLines = 0;
209     return label;
210 #endif
211 }
212
213 static void setBackground(ViewType *view, ColorType *color)
214 {
215 #if PLATFORM(MAC)
216     view.wantsLayer = YES;
217     view.layer.backgroundColor = color.CGColor;
218 #else
219     view.backgroundColor = color;
220 #endif
221 }
222
223 @interface WKSafeBrowsingTextView : TextViewType {
224 @package
225     WeakObjCPtr<WKSafeBrowsingWarning> _warning;
226 }
227 - (instancetype)initWithAttributedString:(NSAttributedString *)attributedString forWarning:(WKSafeBrowsingWarning *)warning;
228 @end
229
230 @implementation WKSafeBrowsingWarning
231
232 - (instancetype)initWithFrame:(RectType)frame safeBrowsingWarning:(const WebKit::SafeBrowsingWarning&)warning completionHandler:(CompletionHandler<void(Variant<WebKit::ContinueUnsafeLoad, URL>&&)>&&)completionHandler
233 {
234     if (!(self = [super initWithFrame:frame])) {
235         completionHandler(WebKit::ContinueUnsafeLoad::Yes);
236         return nil;
237     }
238     _completionHandler = WTFMove(completionHandler);
239     _warning = makeRef(warning);
240     setBackground(self, colorForItem(WarningItem::Background, self));
241 #if PLATFORM(MAC)
242     [self addContent];
243 #endif
244     return self;
245 }
246
247 - (void)addContent
248 {
249     auto exclamationPoint = [[WKSafeBrowsingExclamationPoint new] autorelease];
250     auto title = makeLabel([[[NSAttributedString alloc] initWithString:_warning->title() attributes:@{
251         NSFontAttributeName:[FontType boldSystemFontOfSize:titleSize],
252         NSForegroundColorAttributeName:colorForItem(WarningItem::TitleText, self)
253     }] autorelease]);
254     auto warning = makeLabel([[[NSAttributedString alloc] initWithString:_warning->warning() attributes:@{
255         NSFontAttributeName:[FontType systemFontOfSize:textSize],
256         NSForegroundColorAttributeName:colorForItem(WarningItem::MessageText, self)
257     }] autorelease]);
258     auto showDetails = makeButton(WarningItem::ShowDetailsButton, self, @selector(showDetailsClicked));
259     auto goBack = makeButton(WarningItem::GoBackButton, self, @selector(goBackClicked));
260     auto box = [[ViewType new] autorelease];
261     setBackground(box, colorForItem(WarningItem::BoxBackground, self));
262     box.layer.cornerRadius = boxCornerRadius;
263
264     for (ViewType *view in @[exclamationPoint, title, warning, goBack, showDetails]) {
265         view.translatesAutoresizingMaskIntoConstraints = NO;
266         [box addSubview:view];
267     }
268     box.translatesAutoresizingMaskIntoConstraints = NO;
269     [self addSubview:box];
270
271 #if HAVE(SAFE_BROWSING)
272     [NSLayoutConstraint activateConstraints:@[
273         [[self.topAnchor anchorWithOffsetToAnchor:box.topAnchor] constraintEqualToAnchor:[box.bottomAnchor anchorWithOffsetToAnchor:self.bottomAnchor] multiplier:0.5],
274         [[self.leftAnchor anchorWithOffsetToAnchor:box.leftAnchor] constraintEqualToAnchor:[box.rightAnchor anchorWithOffsetToAnchor:self.rightAnchor]],
275
276         [box.widthAnchor constraintLessThanOrEqualToConstant:maxWidth],
277         [box.widthAnchor constraintLessThanOrEqualToAnchor:self.widthAnchor],
278
279         [[box.leadingAnchor anchorWithOffsetToAnchor:exclamationPoint.leadingAnchor] constraintEqualToConstant:marginSize],
280         [[box.leadingAnchor anchorWithOffsetToAnchor:title.leadingAnchor] constraintEqualToConstant:marginSize * 1.5 + exclamationPointSize],
281         [[box.leadingAnchor anchorWithOffsetToAnchor:warning.leadingAnchor] constraintEqualToConstant:marginSize],
282
283         [[title.trailingAnchor anchorWithOffsetToAnchor:box.trailingAnchor] constraintGreaterThanOrEqualToConstant:marginSize],
284         [[warning.trailingAnchor anchorWithOffsetToAnchor:box.trailingAnchor] constraintGreaterThanOrEqualToConstant:marginSize],
285         [[goBack.trailingAnchor anchorWithOffsetToAnchor:box.trailingAnchor] constraintEqualToConstant:marginSize],
286
287         [[title.topAnchor anchorWithOffsetToAnchor:exclamationPoint.topAnchor] constraintEqualToAnchor:[exclamationPoint.bottomAnchor anchorWithOffsetToAnchor:title.bottomAnchor]],
288
289         [goBack.topAnchor constraintEqualToAnchor:showDetails.topAnchor],
290         [[showDetails.trailingAnchor anchorWithOffsetToAnchor:goBack.leadingAnchor] constraintEqualToConstant:marginSize],
291
292         [[box.topAnchor anchorWithOffsetToAnchor:title.topAnchor] constraintEqualToConstant:marginSize],
293         [[title.bottomAnchor anchorWithOffsetToAnchor:warning.topAnchor] constraintEqualToConstant:marginSize],
294         [[warning.bottomAnchor anchorWithOffsetToAnchor:goBack.topAnchor] constraintEqualToConstant:marginSize],
295         [[goBack.bottomAnchor anchorWithOffsetToAnchor:box.bottomAnchor] constraintEqualToConstant:marginSize]
296     ]];
297 #endif
298 }
299
300 - (void)showDetailsClicked
301 {
302     ViewType *box = self.subviews.lastObject;
303     ButtonType *showDetails = box.subviews.lastObject;
304     [showDetails removeFromSuperview];
305
306     NSMutableAttributedString *text = [[_warning->details() mutableCopy] autorelease];
307     [text addAttributes:@{ NSFontAttributeName:[FontType systemFontOfSize:textSize] } range:NSMakeRange(0, text.length)];
308     WKSafeBrowsingTextView *details = [[[WKSafeBrowsingTextView alloc] initWithAttributedString:text forWarning:self] autorelease];
309     _details = details;
310     ViewType *bottom = [[ViewType new] autorelease];
311     setBackground(bottom, colorForItem(WarningItem::BoxBackground, self));
312     bottom.layer.cornerRadius = boxCornerRadius;
313
314 #if HAVE(SAFE_BROWSING)
315     constexpr auto maxY = kCALayerMinXMaxYCorner | kCALayerMaxXMaxYCorner;
316     constexpr auto minY = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
317 #if PLATFORM(MAC)
318     box.layer.maskedCorners = maxY;
319     bottom.layer.maskedCorners = minY;
320 #else
321     box.layer.maskedCorners = minY;
322     bottom.layer.maskedCorners = maxY;
323 #endif
324 #endif
325
326     ViewType *line = [[ViewType new] autorelease];
327     setBackground(line, [ColorType lightGrayColor]);
328     for (ViewType *view in @[details, bottom, line])
329         view.translatesAutoresizingMaskIntoConstraints = NO;
330
331     [self addSubview:bottom];
332     [bottom addSubview:line];
333     [bottom addSubview:details];
334 #if HAVE(SAFE_BROWSING)
335     [NSLayoutConstraint activateConstraints:@[
336         [box.widthAnchor constraintEqualToAnchor:bottom.widthAnchor],
337         [box.bottomAnchor constraintEqualToAnchor:bottom.topAnchor],
338         [box.leadingAnchor constraintEqualToAnchor:bottom.leadingAnchor],
339         [line.widthAnchor constraintEqualToAnchor:bottom.widthAnchor],
340         [line.leadingAnchor constraintEqualToAnchor:bottom.leadingAnchor],
341         [line.topAnchor constraintEqualToAnchor:bottom.topAnchor],
342         [line.heightAnchor constraintEqualToConstant:1],
343         [[bottom.topAnchor anchorWithOffsetToAnchor:details.topAnchor] constraintEqualToConstant:marginSize],
344         [[details.bottomAnchor anchorWithOffsetToAnchor:bottom.bottomAnchor] constraintEqualToConstant:marginSize],
345         [[bottom.leadingAnchor anchorWithOffsetToAnchor:details.leadingAnchor] constraintEqualToConstant:marginSize],
346         [[details.trailingAnchor anchorWithOffsetToAnchor:bottom.trailingAnchor] constraintEqualToConstant:marginSize],
347     ]];
348 #endif
349     [self layoutText];
350 #if !PLATFORM(MAC)
351     [self layoutIfNeeded];
352     CGFloat height = 0;
353     for (ViewType *subview in self.subviews)
354         height += subview.frame.size.height;
355     [self setContentSize: { self.frame.size.width, self.frame.size.height / 2 + height }];
356 #endif
357 }
358
359 - (void)layoutText
360 {
361     [_details invalidateIntrinsicContentSize];
362 }
363
364 #if PLATFORM(MAC)
365 - (BOOL)textView:(NSTextView *)textView clickedOnLink:(id)link atIndex:(NSUInteger)charIndex
366 {
367     [self clickedOnLink:link];
368     return YES;
369 }
370
371 - (void)layout
372 {
373     [super layout];
374     [self layoutText];
375 }
376 #else
377 - (void)layoutSubviews
378 {
379     [super layoutSubviews];
380     [self layoutText];
381 }
382
383 - (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange interaction:(UITextItemInteraction)interaction
384 {
385     [self clickedOnLink:URL];
386     return NO;
387 }
388
389 - (void)didMoveToWindow
390 {
391     [self addContent];
392 }
393 #endif
394
395 - (void)dealloc
396 {
397     if (_completionHandler)
398         _completionHandler(WebKit::ContinueUnsafeLoad::No);
399     [super dealloc];
400 }
401
402 - (void)goBackClicked
403 {
404     if (_completionHandler)
405         _completionHandler(WebKit::ContinueUnsafeLoad::No);
406 }
407
408 - (void)clickedOnLink:(NSURL *)link
409 {
410     if (!_completionHandler)
411         return;
412
413     if ([link isEqual:WebKit::SafeBrowsingWarning::visitUnsafeWebsiteSentinel()])
414         return _completionHandler(WebKit::ContinueUnsafeLoad::Yes);
415
416     if ([link isEqual:WebKit::SafeBrowsingWarning::confirmMalwareSentinel()]) {
417 #if PLATFORM(MAC)
418         auto alert = adoptNS([NSAlert new]);
419         [alert setMessageText:WEB_UI_NSSTRING(@"Are you sure you wish to go to this site?", "Malware confirmation dialog title")];
420         [alert setInformativeText:WEB_UI_NSSTRING(@"Merely visiting a site is sufficient for malware to install itself and harm your computer.", "Malware confirmation dialog")];
421         [alert addButtonWithTitle:WEB_UI_NSSTRING(@"Cancel", "Cancel")];
422         [alert addButtonWithTitle:WEB_UI_NSSTRING(@"Continue", "Continue")];
423         [alert beginSheetModalForWindow:self.window completionHandler:BlockPtr<void(NSModalResponse)>::fromCallable([weakSelf = WeakObjCPtr<WKSafeBrowsingWarning>(self), alert](NSModalResponse returnCode) {
424             if (auto strongSelf = weakSelf.get()) {
425                 if (returnCode == NSAlertSecondButtonReturn && strongSelf->_completionHandler)
426                     strongSelf->_completionHandler(WebKit::ContinueUnsafeLoad::Yes);
427             }
428         }).get()];
429 #else
430         _completionHandler(WebKit::ContinueUnsafeLoad::Yes);
431 #endif
432         return;
433     }
434
435     ASSERT([link isKindOfClass:[NSURL class]]);
436     _completionHandler((NSURL *)link);
437 }
438
439 @end
440
441 @implementation WKSafeBrowsingTextView
442
443 - (instancetype)initWithAttributedString:(NSAttributedString *)attributedString forWarning:(WKSafeBrowsingWarning *)warning
444 {
445     if (!(self = [super init]))
446         return nil;
447     self->_warning = warning;
448     self.delegate = warning;
449
450     ColorType *foregroundColor = colorForItem(WarningItem::MessageText, warning);
451     NSMutableAttributedString *string = [[attributedString mutableCopy] autorelease];
452     [string addAttributes:@{ NSForegroundColorAttributeName : foregroundColor } range:NSMakeRange(0, string.length)];
453     [self setBackgroundColor:colorForItem(WarningItem::BoxBackground, warning)];
454     [self setLinkTextAttributes:@{ NSForegroundColorAttributeName : foregroundColor }];
455     [self.textStorage appendAttributedString:string];
456     self.editable = NO;
457 #if !PLATFORM(MAC)
458     self.scrollEnabled = NO;
459 #endif
460
461     return self;
462 }
463
464 - (SizeType)intrinsicContentSize
465 {
466 #if PLATFORM(MAC)
467     [self.layoutManager ensureLayoutForTextContainer:self.textContainer];
468     return { NSViewNoIntrinsicMetric, [self.layoutManager usedRectForTextContainer:self.textContainer].size.height };
469 #elif HAVE(SAFE_BROWSING)
470     auto width = std::min<CGFloat>(maxWidth, [_warning frame].size.width) - 2 * marginSize;
471     constexpr auto noHeightConstraint = CGFLOAT_MAX;
472     return { width, [self sizeThatFits: { width, noHeightConstraint }].height };
473 #else
474     return { 0, 0 };
475 #endif
476 }
477
478 @end