Skip to content

Commit

Permalink
Simulate a FileOk handler when choosing an image file
Browse files Browse the repository at this point in the history
This enforces the image file filter even when users type or paste in
a file path that defeats the filter.  This fix is motivated by
https://issues.bloomlibrary.org/youtrack/issue/BL-13552.  Using the
DialogAdapters interface prevents having an actual FileOk handler.
  • Loading branch information
StephenMcConnel committed Aug 1, 2024
1 parent e5745ec commit 5f048f8
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Fixed

- [SIL.Window.Forms] When choosing a file in the ImageToolbox.AcquireImageControl, a FileOk handler is simulated that verifies the selected file passes the given filter. Users can defeat the filter mechanism by pasting or typing the file name. While the returned filename does not pass the filter, the dialog is reopened until the user either chooses a proper filename or cancels the dialog. The native FileOk handler can prevent the dialog from closing: we can't achieve that. (See BL-13552.)
- [SIL.Windows.Forms.DblBundle] Fixed bug in ProjectsListBase that made it impossible to select a project after double-clicking a column header. (See HT-475)

## [14.1.0] - 2024-05-13
Expand Down
72 changes: 72 additions & 0 deletions SIL.Windows.Forms.Tests/ImageToolbox/ImageToolboxTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,77 @@ public void CheckMonoForSelectLargeIconView()
Assert.IsNotNull(viewOnClickMethod, "MWFFileView no longer has an OnClickViewMenuSubItem(object, EventArgs) method");
}
}

[Test]
public void DoubleCheckFileFilterWorks()
{
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(null, "foo.txt"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(null, "foo.doc"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(null, "foo"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(null, "foo."));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(null, "foo.bar"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(null, "foo.bar.baz"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter("", "foo.txt"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter("", "foo.doc"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter("", "foo"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter("", "foo."));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter("", "foo.bar"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter("", "foo.bar.baz"));

var filterAll = "All files|*.*";
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterAll, "foo.txt"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterAll, "foo.doc"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterAll, "foo"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterAll, "foo."));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterAll, "foo.bar"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterAll, "foo.bar.baz"));

var filterTxtOnly = "Text files|*.txt";
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterTxtOnly, "foo.txt"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterTxtOnly, "foo.doc"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterTxtOnly, "foo"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterTxtOnly, "foo."));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterTxtOnly, "foo.bar"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterTxtOnly, "foo.txt.baz"));

var filterTxtAndDoc = "Text files|*.txt|Word files|*.doc";
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDoc, "foo.txt"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDoc, "foo.doc"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDoc, "foo"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDoc, "foo."));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDoc, "foo.bar"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDoc, "foo.txt.baz"));

var filterTxtAndDocWithAll = "Text files|*.txt|Word files|*.doc|All files|*.*";
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDocWithAll, "foo.txt"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDocWithAll, "foo.doc"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDocWithAll, "foo"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDocWithAll, "foo."));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDocWithAll, "foo.bar"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterTxtAndDocWithAll, "foo.txt.baz"));

var filterCodeSourceFiles = "Image files|*.png;*.jpg;*.jpeg;*.tiff";
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterCodeSourceFiles, "foo.png"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterCodeSourceFiles, "foo.jpg"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterCodeSourceFiles, "foo.jpeg"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterCodeSourceFiles, "foo.tiff"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterCodeSourceFiles, "foo"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterCodeSourceFiles, "foo."));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterCodeSourceFiles, "foo.bar"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterCodeSourceFiles, "foo.png.baz"));

var filterCsCppSourceFiles = "Bitmap files|*.bmp;*.png;*.tiff|JPEG files|*.jpg;*.jpeg";
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo.bmp"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo.png"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo.tiff"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo.jpg"));
Assert.IsTrue(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo.jpeg"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo.h"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo.cs"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo."));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo.bar"));
Assert.IsFalse(AcquireImageControl.DoubleCheckFileFilter(filterCsCppSourceFiles, "foo.png.baz"));
}
}
}
131 changes: 96 additions & 35 deletions SIL.Windows.Forms/ImageToolbox/AcquireImageControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,61 +81,122 @@ private void GetImageFileFromSystem()
{
try
{
// Used to simulate having a FileOk event handler that can cancel the OK button click.
bool invalidFileChosen;
SetMode(Modes.SingleImage);
// The primary thing that OpenFileDialogWithViews buys us is the ability to default to large icons.
// OpenFileDialogWithViews still doesn't let us read (and thus remember) the selected view.
using (var dlg = new OpenFileDialogWithViews(OpenFileDialogWithViews.DialogViewTypes.Large_Icons))
do
{
if (string.IsNullOrEmpty(ImageToolboxSettings.Default.LastImageFolder))
invalidFileChosen = false;
// The primary thing that OpenFileDialogWithViews buys us is the ability to default to large icons.
// OpenFileDialogWithViews still doesn't let us read (and thus remember) the selected view.
using (var dlg = new OpenFileDialogWithViews(OpenFileDialogWithViews.DialogViewTypes.Large_Icons))
{
dlg.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
}
else
{
dlg.InitialDirectory = ImageToolboxSettings.Default.LastImageFolder; ;
}

dlg.Title = "Choose Picture File".Localize("ImageToolbox.FileChooserTitle", "Title shown for a file chooser dialog brought up by the ImageToolbox");
if (string.IsNullOrEmpty(ImageToolboxSettings.Default.LastImageFolder))
{
dlg.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
}
else
{
dlg.InitialDirectory = ImageToolboxSettings.Default.LastImageFolder; ;
}

//NB: disallowed gif because of a .net crash: http://jira.palaso.org/issues/browse/BL-85
dlg.Filter = "picture files".Localize("ImageToolbox.PictureFiles", "Shown in the file-picking dialog to describe what kind of files the dialog is filtering for") + "(*.png;*.tif;*.tiff;*.jpg;*.jpeg;*.bmp)|*.png;*.tif;*.tiff;*.jpg;*.jpeg;*.bmp;";
dlg.Title = "Choose Picture File".Localize("ImageToolbox.FileChooserTitle", "Title shown for a file chooser dialog brought up by the ImageToolbox");

if (DialogResult.OK == dlg.ShowDialog(this.ParentForm))
{
ImageToolboxSettings.Default.LastImageFolder = Path.GetDirectoryName(dlg.FileName);
ImageToolboxSettings.Default.Save();
//NB: disallowed gif because of a .net crash: http://jira.palaso.org/issues/browse/BL-85
dlg.Filter = "picture files".Localize("ImageToolbox.PictureFiles", "Shown in the file-picking dialog to describe what kind of files the dialog is filtering for") + "(*.png;*.tif;*.tiff;*.jpg;*.jpeg;*.bmp)|*.png;*.tif;*.tiff;*.jpg;*.jpeg;*.bmp;";

try
{
_currentImage = PalasoImage.FromFile(dlg.FileName);
}
catch (Exception err) //for example, http://jira.palaso.org/issues/browse/BL-199
if (DialogResult.OK == dlg.ShowDialog(this.ParentForm))
{
var msg = "Sorry, there was a problem loading that image.".Localize(
"ImageToolbox.ProblemLoadingImage");
if (ImageLoadingExceptionReporter!= null)
// Simulate having a FileOk event handler that can cancel the OK button click.
if (!DoubleCheckFileFilter(dlg.Filter, dlg.FileName))
{
invalidFileChosen = true;
continue;
}
ImageToolboxSettings.Default.LastImageFolder = Path.GetDirectoryName(dlg.FileName);
ImageToolboxSettings.Default.Save();

try
{
ImageLoadingExceptionReporter(dlg.FileName, err, msg);
_currentImage = PalasoImage.FromFile(dlg.FileName);
}
else
catch (Exception err) //for example, http://jira.palaso.org/issues/browse/BL-199
{
ErrorReport.NotifyUserOfProblem(err, msg);
var msg = "Sorry, there was a problem loading that image.".Localize(
"ImageToolbox.ProblemLoadingImage");
if (ImageLoadingExceptionReporter!= null)
{
ImageLoadingExceptionReporter(dlg.FileName, err, msg);
}
else
{
ErrorReport.NotifyUserOfProblem(err, msg);
}

return;
}

return;
_pictureBox.Image = _currentImage.Image;
if (ImageChanged != null)
ImageChanged.Invoke(this, null);
}
_pictureBox.Image = _currentImage.Image;
if (ImageChanged != null)
ImageChanged.Invoke(this, null);
}
}
} while (invalidFileChosen);
}
finally
{
_actModal = false;
}
}

/// <summary>
/// Return true if the filePath truly passes the filtering of the filterString.
/// People can defeat the filtering by typing or pasting a file path into the file path box.
/// </summary>
/// <param name="filterString">filter string like those used in file dialogs</param>
/// <param name="filePath">file path returned by a file dialog</param>
internal static bool DoubleCheckFileFilter(string filterString, string filePath)
{
if (string.IsNullOrEmpty(filterString))
return true; // no filter, so everything passes
if (string.IsNullOrEmpty(filePath))
return false; // no file, so nothing passes
var filterSections = filterString.Split('|');
if (filterSections.Length < 2)
return true; // no filter, so everything passes
var fileName = Path.GetFileName(filePath);
for (int i = 1; i < filterSections.Length; i += 2)
{
if (PassesFilter(filterSections[i], fileName))
return true;
}
return false;
}
private static bool PassesFilter(string filterList, string fileName)
{
var parts = filterList.Split(';');
foreach (var part in parts)
{
if (part == "*.*" || part == "*")
return true;
var filter = part.Trim();
if (filter.StartsWith("*"))
{
filter = filter.Substring(1);
if (fileName.EndsWith(filter, StringComparison.InvariantCultureIgnoreCase))
return true;
}
else if (filter.EndsWith("*"))
{
filter = filter.Substring(0, filter.Length - 1);
if (fileName.StartsWith(filter, StringComparison.InvariantCultureIgnoreCase))
return true;
}
else if (fileName.Equals(filter, StringComparison.InvariantCultureIgnoreCase))
return true;
}
return false;
}

private void OpenFileFromDrag(string path)
{
SetMode(Modes.SingleImage);
Expand Down

0 comments on commit 5f048f8

Please sign in to comment.