Skip to content

Commit

Permalink
6.4.5 (#1258)
Browse files Browse the repository at this point in the history
- Fix out-of-memory crash when parsing link previews
- Fix display of qr-code button when viewing own omemo keys
- Make discovery of group/channel names more reliable
- Don't crash on errors when adding a new account
- Don't leave messages in "Sending..." state after resuming the app from
background
- Allow adding gateway jids as contacts
- Make group/channel detection more reliable when adding via jid
  • Loading branch information
tmolitor-stud-tu authored Oct 15, 2024
2 parents 65853b8 + 085f90d commit 18ca827
Show file tree
Hide file tree
Showing 21 changed files with 933 additions and 108 deletions.
52 changes: 52 additions & 0 deletions .github/workflows/pr-semver-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Check PR title for proper semver

on:
pull_request:
branches:
- beta
- stable

workflow_dispatch:
inputs:
pr_number:
description: "Pull request number to check"
required: true
type: number

jobs:
check-pr-semver-title:
runs-on: ubuntu-latest
name: Validate PR Title

steps:
- name: Get PR details
id: find_pr
run: |
if [ -z "${{ github.event.inputs.pr_number }}" ]; then
prNumber=${{ github.event.pull_request.number }}
else
prNumber=${{ github.event.inputs.pr_number }}
fi
echo "prNumber=$prNumber" | tee /dev/stderr >> "$GITHUB_OUTPUT"
- name: Fetch pull request title
id: pr_title
uses: actions/github-script@v7
with:
script: |
const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/");
const prNumber = ${{ steps.find_pr.outputs.prNumber }};
const { data: pull_request } = await github.rest.pulls.get({
owner: owner,
repo: repo,
pull_number: prNumber
});
return pull_request.title;
- name: Check PR title format
run: |
version="${{ steps.pr_title.outputs.result }}"
if ! [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then
echo "Invalid semver: '$version'!"
exit 1
fi
echo "Version is proper semver: $version"
9 changes: 0 additions & 9 deletions Monal/Classes/AccountListController.m
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ -(void) refreshAccountList
-(void) initContactCell:(MLSwitchCell*) cell forAccNo:(NSUInteger) accNo
{
[cell initTapCell:@"\n\n"];
cell = [cell initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"AccountCell"];
NSDictionary* account = [self.accountList objectAtIndex:accNo];
MLAssert(account != nil, ([NSString stringWithFormat:@"Expected non nil account in row %lu", (unsigned long)accNo]));
if([(NSString*)[account objectForKey:@"domain"] length] > 0) {
Expand All @@ -89,7 +88,6 @@ -(void) initContactCell:(MLSwitchCell*) cell forAccNo:(NSUInteger) accNo
}

UIImageView* accessory = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
cell.detailTextLabel.text = nil;

if([[account objectForKey:@"enabled"] boolValue] == YES)
{
Expand All @@ -98,25 +96,18 @@ -(void) initContactCell:(MLSwitchCell*) cell forAccNo:(NSUInteger) accNo
{
accessory.image = [UIImage imageNamed:@"Connected"];
cell.accessoryView = accessory;

NSDate* connectedTime = [[MLXMPPManager sharedInstance] connectedTimeFor:[[self.accountList objectAtIndex:accNo] objectForKey:@"account_id"]];
if(connectedTime) {
cell.detailTextLabel.text = [NSString stringWithFormat:NSLocalizedString(@"Connected since: %@", @""), [self.uptimeFormatter stringFromDate:connectedTime]];
}
}
else
{
accessory.image = [UIImage imageNamed:@"Disconnected"];
cell.accessoryView = accessory;
cell.detailTextLabel.text = NSLocalizedString(@"Connecting...", @"");
}
}
else
{
cell.imageView.image = [UIImage systemImageNamed:@"circle"];
accessory.image = nil;
cell.accessoryView = accessory;
cell.detailTextLabel.text = NSLocalizedString(@"Account disabled", @"");
}
}

Expand Down
13 changes: 12 additions & 1 deletion Monal/Classes/MLDelayableTimer.m
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,15 @@ -(void) pause
DDLogWarn(@"Tried to pause already fired timer: %@", self);
return;
}
NSTimeInterval remaining = _wrappedTimer.fireDate.timeIntervalSinceNow;
if(remaining == 0)
{
DDLogWarn(@"Tried to pause timer the exact second its firing: %@", self);
return;
}
DDLogDebug(@"Pausing timer: %@", self);
_remainingTime = _wrappedTimer.fireDate.timeIntervalSinceNow;
_wrappedTimer.fireDate = NSDate.distantFuture; //postpone timer virtually indefinitely
_remainingTime = remaining;
}
}

Expand All @@ -91,6 +97,11 @@ -(void) resume
DDLogWarn(@"Tried to resume already fired timer: %@", self);
return;
}
if(_remainingTime == 0)
{
DDLogWarn(@"Tried to resume non-paused timer: %@", self);
return;
}
DDLogDebug(@"Resuming timer: %@", self);
_wrappedTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:_remainingTime];
_remainingTime = 0;
Expand Down
2 changes: 1 addition & 1 deletion Monal/Classes/MLMucProcessor.m
Original file line number Diff line number Diff line change
Expand Up @@ -1417,7 +1417,7 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room
}

//extract further muc infos
NSString* mucName = [iqNode findFirst:@"{http://jabber.org/protocol/disco#info}query/\\{http://jabber.org/protocol/muc#roominfo}result@muc#roomconfig_roomname\\"];
NSString* mucName = [iqNode findFirst:@"{http://jabber.org/protocol/disco#info}query/identity@name"];
NSString* mucType = @"channel";
//both are needed for omemo, see discussion with holger 2021-01-02/03 -- Thilo Molitor
//see also: https://docs.modernxmpp.org/client/groupchat/
Expand Down
48 changes: 20 additions & 28 deletions Monal/Classes/MLOgHtmlParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,32 @@
// Copyright © 2022 Monal.im. All rights reserved.
//

import SwiftSoup;

@objc class MLOgHtmlParser: NSObject {
var og_title: String?
var og_image_url: URL?

@objc init(html: String, andBaseUrl baseUrl: URL?) {
super.init()
do {
let parsedSite: Document = try SwiftSoup.parse(html)

self.og_title = try parsedSite.select("meta[property=og:title]").first()?.attr("content")
if self.og_title == nil {
self.og_title = try parsedSite.select("html head title").first()?.text()
}
if self.og_title == nil {
DDLogWarn("Could not find any site title")
}

if let image_url = try parsedSite.select("meta[property=og:image]").first()?.attr("content").removingPercentEncoding {
self.og_image_url = self.parseUrl(image_url, baseUrl)
} else if let image_url = try parsedSite.select("html head link[rel=apple-touch-icon]").first()?.attr("href").removingPercentEncoding {
self.og_image_url = self.parseUrl(image_url, baseUrl)
} else if let image_url = try parsedSite.select("html head link[rel=icon]").first()?.attr("href").removingPercentEncoding {
self.og_image_url = self.parseUrl(image_url, baseUrl)
} else if let image_url = try parsedSite.select("html head link[rel=shortcut icon]").first()?.attr("href").removingPercentEncoding {
self.og_image_url = self.parseUrl(image_url, baseUrl)
} else {
DDLogWarn("Could not find any site image")
}
} catch Exception.Error(let type, let message) {
DDLogWarn("Could not parse html og elements: \(message) type: \(type)")
} catch {
DDLogWarn("Could not parse html og elements: unhandled exception")
let parsedSite = HtmlParserBridge(html:html)

self.og_title = try? parsedSite.select("meta[property=og\\:title]", attribute:"content").first
if self.og_title == nil {
self.og_title = try? parsedSite.select("html head title").first
}
if self.og_title == nil {
DDLogWarn("Could not find any site title")
}

if let image_url = try? parsedSite.select("meta[property=og\\:image]", attribute:"content").first?.removingPercentEncoding {
self.og_image_url = self.parseUrl(image_url, baseUrl)
} else if let image_url = try? parsedSite.select("html head link[rel=apple-touch-icon]", attribute:"href").first?.removingPercentEncoding {
self.og_image_url = self.parseUrl(image_url, baseUrl)
} else if let image_url = try? parsedSite.select("html head link[rel=icon]", attribute:"href").first?.removingPercentEncoding {
self.og_image_url = self.parseUrl(image_url, baseUrl)
} else if let image_url = try? parsedSite.select("html head link[rel=shortcut icon]", attribute:"href").first?.removingPercentEncoding {
self.og_image_url = self.parseUrl(image_url, baseUrl)
} else {
DDLogWarn("Could not find any site image in html")
}
}

Expand Down
43 changes: 24 additions & 19 deletions Monal/Classes/MLStream.m
Original file line number Diff line number Diff line change
Expand Up @@ -687,25 +687,30 @@ -(void) generateEvent:(NSStreamEvent) event
//don't schedule delegate calls if no runloop was specified
if(self.shared_state.runLoop == nil)
return;
//schedule the delegate calls in the runloop that was registered
CFRunLoopPerformBlock([self.shared_state.runLoop getCFRunLoop], (__bridge CFStringRef)self.shared_state.runLoopMode, ^{
@synchronized(self.shared_state) {
if(event == NSStreamEventOpenCompleted && self.open_called && self.shared_state.open)
[self->_delegate stream:self handleEvent:event];
else if(event == NSStreamEventHasBytesAvailable && self.open_called && self.shared_state.open)
[self->_delegate stream:self handleEvent:event];
else if(event == NSStreamEventHasSpaceAvailable && self.open_called && self.shared_state.open)
[self->_delegate stream:self handleEvent:event];
else if(event == NSStreamEventErrorOccurred)
[self->_delegate stream:self handleEvent:event];
else if(event == NSStreamEventEndEncountered && self.open_called && self.shared_state.open)
[self->_delegate stream:self handleEvent:event];
else
DDLogVerbose(@"Ignored event %ld", (long)event);
}
});
//trigger wakeup of runloop to execute the block as soon as possible
CFRunLoopWakeUp([self.shared_state.runLoop getCFRunLoop]);
//make sure to NOT hold the @synchronized lock when calling the delegate to not introduce deadlocks
BOOL handleEvent = NO;
if(event == NSStreamEventOpenCompleted && self.open_called && self.shared_state.open)
handleEvent = YES;
else if(event == NSStreamEventHasBytesAvailable && self.open_called && self.shared_state.open)
handleEvent = YES;
else if(event == NSStreamEventHasSpaceAvailable && self.open_called && self.shared_state.open)
handleEvent = YES;
else if(event == NSStreamEventErrorOccurred)
handleEvent = YES;
else if(event == NSStreamEventEndEncountered && self.open_called && self.shared_state.open)
handleEvent = YES;
//check if the event should be handled
if(!handleEvent)
DDLogVerbose(@"Ignoring event %ld", (long)event);
else
{
//schedule the delegate calls in the runloop that was registered
CFRunLoopPerformBlock([self.shared_state.runLoop getCFRunLoop], (__bridge CFStringRef)self.shared_state.runLoopMode, ^{
[self->_delegate stream:self handleEvent:event];
});
//trigger wakeup of runloop to execute the block as soon as possible
CFRunLoopWakeUp([self.shared_state.runLoop getCFRunLoop]);
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions Monal/Classes/MonalAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,10 @@ -(void) showConnectionStatus:(NSNotification*) notification
{
dispatch_async(dispatch_get_main_queue(), ^{
xmpp* xmppAccount = notification.object;
//ignore errors with unknown accounts
//(possibly meaning an account we currently try to create --> the creating ui will take care of this already)
if(xmppAccount == nil)
return;
if(![notification.userInfo[@"isSevere"] boolValue])
DDLogError(@"Minor XMPP Error(%@): %@", xmppAccount.connectionProperties.identity.jid, notification.userInfo[@"message"]);
NotificationBanner* banner = [[NotificationBanner alloc] initWithTitle:xmppAccount.connectionProperties.identity.jid subtitle:notification.userInfo[@"message"] leftView:nil rightView:nil style:([notification.userInfo[@"isSevere"] boolValue] ? BannerStyleDanger : BannerStyleWarning) colors:nil];
Expand Down
39 changes: 39 additions & 0 deletions Monal/Classes/SwiftHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,32 @@ public class SwiftHelpers: NSObject {
}
}

//TODO: remove this
extension UIImage {
public func thumbnail(size: CGSize) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
defer { UIGraphicsEndImageContext() }
draw(in: CGRect(origin: .zero, size: size))
return UIGraphicsGetImageFromCurrentImageContext()
}
}

// **********************************************
// **************** rust bridges ****************
// **********************************************

fileprivate extension RustVec {
func intoArray() -> [T] {
var array: [T] = []
for _ in 0..<self.len() {
array.append(self.pop()!)
}
return array.reversed()
}
}

extension RustString: Error {}

@objcMembers
public class JingleSDPBridge : NSObject {
@objc(getJingleStringForSDPString:withInitiator:)
Expand All @@ -420,3 +446,16 @@ public class JingleSDPBridge : NSObject {
return nil
}
}

@objcMembers
public class HtmlParserBridge : NSObject {
var document: MonalHtmlParser

public init(html: String) {
self.document = MonalHtmlParser(html)
}

public func select(_ selector: String, attribute: String? = nil) throws -> [String] {
return self.document.select(selector, attribute).intoArray().map { $0.toString() }
}
}
4 changes: 2 additions & 2 deletions Monal/Classes/SwiftuiHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -656,9 +656,9 @@ class SwiftuiInterface : NSObject {
func makeOwnOmemoKeyView(_ ownContact: MLContact?) -> UIViewController {
let host = UIHostingController(rootView:AnyView(EmptyView()))
if(ownContact == nil) {
host.rootView = AnyView(OmemoKeys(contact: nil))
host.rootView = AnyView(UIKitWorkaround(OmemoKeys(contact: nil)))
} else {
host.rootView = AnyView(OmemoKeys(contact: ObservableKVOWrapper<MLContact>(ownContact!)))
host.rootView = AnyView(UIKitWorkaround(OmemoKeys(contact: ObservableKVOWrapper<MLContact>(ownContact!))))
}
return host
}
Expand Down
7 changes: 5 additions & 2 deletions Monal/Classes/chatViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -3060,10 +3060,13 @@ -(void) downloadPreviewWithRow:(NSIndexPath*) indexPath usingByterange:(BOOL) us
else
{
NSString* body = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSURL* baseURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@%@", row.url.scheme, row.url.host, row.url.path]];
MLOgHtmlParser* ogParser = [[MLOgHtmlParser alloc] initWithHtml:body andBaseUrl:baseURL];
MLOgHtmlParser* ogParser = nil;
NSString* text = nil;
NSURL* image = nil;
if([body length] > 524288)
body = [body substringToIndex:524288];
NSURL* baseURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@%@", row.url.scheme, row.url.host, row.url.path]];
ogParser = [[MLOgHtmlParser alloc] initWithHtml:body andBaseUrl:baseURL];
if(ogParser != nil)
{
text = [ogParser getOgTitle];
Expand Down
13 changes: 10 additions & 3 deletions Monal/Classes/xmpp.m
Original file line number Diff line number Diff line change
Expand Up @@ -2955,7 +2955,7 @@ -(void) handleFeaturesBeforeAuth:(MLXMLNode*) parsedStanza withForceSasl2:(BOOL)
//leave that in for translators, we might use it at a later time
while(!NSLocalizedString(@"This server isn't additionally hardened against man-in-the-middle attacks on the TLS encryption layer by using authentication methods that are secure against such attacks! This indicates an ongoing attack if the server is supposed to support SASL2 and SCRAM and is harmless otherwise. Use the advanced account creation menu and turn on the PLAIN switch there if you still want to log in to this server.", @""));

clearPipelineCacheOrReportSevereError(NSLocalizedString(@"This server lacks support for SASL2 and SCRAM, additionally hardening authentication against man-in-the-middle attacks on the TLS encryption layer. Since this server is listed as supporting both at https://github.com/monal-im/SCRAM_PreloadList, an ongoing MITM attack is highly likely! You should try again once you are in a clean networking environment.", @""));
clearPipelineCacheOrReportSevereError(NSLocalizedString(@"This server lacks support for SASL2 and SCRAM, additionally hardening authentication against man-in-the-middle attacks on the TLS encryption layer. Since this server is listed as supporting both at https://github.com/monal-im/SCRAM_PreloadList (or you intentionally left the PLAIN switch off when using the advanced account creation menu), an ongoing MITM attack is very likely! Try again once you are in a clean network environment.", @""));
return;
}
}
Expand Down Expand Up @@ -4471,9 +4471,16 @@ -(AnyPromise*) checkJidType:(NSString*) jid
[discoInfo setiqTo:jid];
[discoInfo setDiscoInfoNode];
[self sendIq:discoInfo withResponseHandler:^(XMPPIQ* response) {
NSSet* identities = [NSSet setWithArray:[response find:@"{http://jabber.org/protocol/disco#info}query/identity@category"]];
NSSet* features = [NSSet setWithArray:[response find:@"{http://jabber.org/protocol/disco#info}query/feature@var"]];
//check if this is a muc or account
if([features containsObject:@"http://jabber.org/protocol/muc"])
//check if this is an account or a muc
//this test has to come first because a gateway component may have an "account" identity while also supporintg MUC.
//usually this means that there's a bot at the component's address that facilitates registration without adhoc commands.
//the "account" jidType makes it possible to add the component as a contact.
if([identities containsObject:@"account"])
return resolve(@"account");
else if([identities containsObject:@"conference"]
&& [features containsObject:@"http://jabber.org/protocol/muc"])
return resolve(@"muc");
else
return resolve(@"account");
Expand Down
Loading

0 comments on commit 18ca827

Please sign in to comment.