macOS: Avoid triggering TCC permission dialogs in file dialogs

In our NSOpenSavePanelDelegate we respond to panel:shouldEnableURL: by
checking the file dialog's filter options. As part of this, we pulled
out the file's attributes using [NSFileManager attributesOfItemAtPath:],
but this API triggers the TCC (Transparency, Consent, and Control)
machinery to ask the user for permission to access the path in question.

We could replace the directory check with fileExistsAtPath:isDirectory:,
but this would still leave the checks for writable/readable/executable.

Luckily for us, the plumbing for QFileInfo uses lower level CoreFoundation
APIs that don't have these issues (except for isBundle, which we should
fix separately).

This also means we can remove the custom isHiddenFileAtURL helper, as
it was based on the same kCFURLIsHiddenKey as the QFileInfo plumbing.

Fixes: QTBUG-114919
Pick-to: 6.5 6.6
Change-Id: I9ebefaeb1ef7bcc5bb9a1c5cd4b993ce230cf506
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Tor Arne Vestbø 2023-06-29 17:29:24 +02:00
parent 8fbce6b4a0
commit 90345459fc

View File

@ -194,19 +194,6 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions;
[m_panel close]; [m_panel close];
} }
- (BOOL)isHiddenFileAtURL:(NSURL *)url
{
BOOL hidden = NO;
if (url) {
CFBooleanRef isHiddenProperty;
if (CFURLCopyResourcePropertyForKey((__bridge CFURLRef)url, kCFURLIsHiddenKey, &isHiddenProperty, nullptr)) {
hidden = CFBooleanGetValue(isHiddenProperty);
CFRelease(isHiddenProperty);
}
}
return hidden;
}
- (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url
{ {
Q_UNUSED(sender); Q_UNUSED(sender);
@ -215,24 +202,21 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions;
if (!filename.length) if (!filename.length)
return NO; return NO;
// Always accept directories regardless of their names (unless it is a bundle): QFileInfo fileInfo(QString::fromNSString(filename));
NSFileManager *fm = NSFileManager.defaultManager;
NSDictionary *fileAttrs = [fm attributesOfItemAtPath:filename error:nil]; // Always accept directories regardless of their names.
if (!fileAttrs) // This also includes symlinks and aliases to directories.
return NO; // Error accessing the file means 'no'. if (fileInfo.isDir()) {
NSString *fileType = fileAttrs.fileType; // Unless it's a bundle, and we should treat bundles as files.
bool isDir = [fileType isEqualToString:NSFileTypeDirectory]; // FIXME: We'd like to use QFileInfo::isBundle() here, but the
if (isDir) { // detection in QFileInfo goes deeper than NSWorkspace does
// (likely a bug), and as a result causes TCC permission
// dialogs to pop up when used.
bool treatBundlesAsFiles = !m_panel.treatsFilePackagesAsDirectories; bool treatBundlesAsFiles = !m_panel.treatsFilePackagesAsDirectories;
if (!(treatBundlesAsFiles && [NSWorkspace.sharedWorkspace isFilePackageAtPath:filename])) if (!(treatBundlesAsFiles && [NSWorkspace.sharedWorkspace isFilePackageAtPath:filename]))
return YES; return YES;
} }
// Treat symbolic links and aliases to directories like directories
QFileInfo fileInfo(QString::fromNSString(filename));
if (fileInfo.isSymLink() && QFileInfo(fileInfo.symLinkTarget()).isDir())
return YES;
QString qtFileName = fileInfo.fileName(); QString qtFileName = fileInfo.fileName();
// No filter means accept everything // No filter means accept everything
bool nameMatches = m_selectedNameFilter->isEmpty(); bool nameMatches = m_selectedNameFilter->isEmpty();
@ -245,22 +229,22 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions;
return NO; return NO;
QDir::Filters filter = m_options->filter(); QDir::Filters filter = m_options->filter();
if ((!(filter & (QDir::Dirs | QDir::AllDirs)) && isDir) if ((!(filter & (QDir::Dirs | QDir::AllDirs)) && fileInfo.isDir())
|| (!(filter & QDir::Files) && [fileType isEqualToString:NSFileTypeRegular]) || (!(filter & QDir::Files) && (fileInfo.isFile() && !fileInfo.isSymLink()))
|| ((filter & QDir::NoSymLinks) && [fileType isEqualToString:NSFileTypeSymbolicLink])) || ((filter & QDir::NoSymLinks) && fileInfo.isSymLink()))
return NO; return NO;
bool filterPermissions = ((filter & QDir::PermissionMask) bool filterPermissions = ((filter & QDir::PermissionMask)
&& (filter & QDir::PermissionMask) != QDir::PermissionMask); && (filter & QDir::PermissionMask) != QDir::PermissionMask);
if (filterPermissions) { if (filterPermissions) {
if ((!(filter & QDir::Readable) && [fm isReadableFileAtPath:filename]) if ((!(filter & QDir::Readable) && fileInfo.isReadable())
|| (!(filter & QDir::Writable) && [fm isWritableFileAtPath:filename]) || (!(filter & QDir::Writable) && fileInfo.isWritable())
|| (!(filter & QDir::Executable) && [fm isExecutableFileAtPath:filename])) || (!(filter & QDir::Executable) && fileInfo.isExecutable()))
return NO; return NO;
} }
if (!(filter & QDir::Hidden)
&& (qtFileName.startsWith(u'.') || [self isHiddenFileAtURL:url])) if (!(filter & QDir::Hidden) && fileInfo.isHidden())
return NO; return NO;
return YES; return YES;
} }