Skip to content

Commit

Permalink
Expand binary field and a test. (#976)
Browse files Browse the repository at this point in the history
* Expand binary field and a test.

* Use DocumentBinaryProvider, and size limit.

* Use settings defaults.

* Expandable extension setting became an array and it is not pinned to a static variable.

* The Binary of ".settings" is expandable.

* Binary of "*.settings" is expandable.

* Use default settings and heuristic "is text" algorithm.

Co-authored-by: tusmester <tusmester@gmail.com>
  • Loading branch information
kavics and tusmester committed May 28, 2020
1 parent ec40ee8 commit 2368329
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/OData/ExpanderProjector.cs
Expand Up @@ -6,8 +6,11 @@
using SenseNet.ContentRepository.Storage;
using SenseNet.ContentRepository.Fields;
using System.Diagnostics;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Http;
using SenseNet.OData.Writers;
using SenseNet.Portal.Virtualization;
using SenseNet.Search;
// ReSharper disable ArrangeThisQualifier

Expand Down Expand Up @@ -241,6 +244,8 @@ private object Project(Field field, List<Property> expansion, List<Property> sel
{
if (!(field is ReferenceField refField))
{
if (field is BinaryField binaryField)
return TextFileHandler.ProjectBinaryField(binaryField, selection.Select(x=>x.Name).ToArray(), httpContext);
if (!(field is AllowedChildTypesField allowedChildTypesField))
return null;
return ProjectMultiRefContents(allowedChildTypesField.GetData(), expansion, selection, httpContext);
Expand All @@ -255,6 +260,7 @@ private object Project(Field field, List<Property> expansion, List<Property> sel
? ProjectMultiRefContents(refField.GetData(), expansion, selection, httpContext)
: (object)ProjectSingleRefContent(refField.GetData(), expansion, selection, httpContext);
}

private List<ODataEntity> ProjectMultiRefContents(object references, List<Property> expansion, List<Property> selection, HttpContext httpContext)
{
var contents = new List<ODataEntity>();
Expand Down
2 changes: 2 additions & 0 deletions src/OData/SimpleExpanderProjector.cs
Expand Up @@ -131,6 +131,8 @@ private object Project(Field field, List<Property> expansion, HttpContext httpCo
{
if (!(field is ReferenceField refField))
{
if (field is BinaryField binaryField)
return TextFileHandler.ProjectBinaryField(binaryField, null, httpContext);
if (!(field is AllowedChildTypesField allowedChildTypesField))
return null;
return ProjectMultiRefContents(allowedChildTypesField.GetData(), expansion, httpContext);
Expand Down
108 changes: 108 additions & 0 deletions src/OData/TextFileHandler.cs
@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Http;
using SC = SenseNet.ContentRepository;
using SenseNet.ContentRepository.Fields;
using SenseNet.Portal.Virtualization;

namespace SenseNet.OData
{

internal class TextFileHandler
{
private static class Expansion
{
public static string FileName = "FileName";
public static string ContentType = "ContentType";
public static string Length = "Length";
public static string Message = "Message";
public static string Text = "Text";
}
private static class Settings
{
public static string SettingsName = "TextFiles";
public static string Extensions = "Extensions";
public static string MaxExpandableSize = "MaxExpandableSize";
}

private static readonly string[] DefaultTextFileExtensions = { /*"md", "txt", "js", "settings"*/ };

private static string[] TextFileExtensions => SC.Settings.GetValue(
Settings.SettingsName,
Settings.Extensions,
null, DefaultTextFileExtensions);

private static readonly char[] Whitespaces = "\t\r\n".ToCharArray();

internal static object ProjectBinaryField(BinaryField field, string[] selection, HttpContext httpContext)
{
var allSelected = selection == null || selection.Length == 0 || selection[0] == "*";

var stream = DocumentBinaryProvider.Instance.GetStream(field.Content.ContentHandler,
field.Name, httpContext, out var contentType, out var binaryFileName);

var message = string.Empty;
var contentName = field.Content.Name;
var extension = Path.GetExtension(contentName)?.Trim('.');
var maxSize = SC.Settings.GetValue(Settings.SettingsName, Settings.MaxExpandableSize,
null, 1024 * 1024);

if (stream.Length > maxSize)
{
message = $"Size limit exceed. Limit: {maxSize}, size: {stream.Length}";
}
else
{
var whitelist = TextFileExtensions;
if (whitelist.Length > 0 && !whitelist.Contains(extension, StringComparer.OrdinalIgnoreCase))
message = $"Not a text file. The *.{extension} is restricted by the file extension list.";
}

var text = string.IsNullOrEmpty(message)
? ReadBinaryContent(stream, out message)
: null;

var result = new Dictionary<string, object>();
if (allSelected || selection.Contains(Expansion.FileName))
result.Add(Expansion.FileName, binaryFileName.ToString());
if (allSelected || selection.Contains(Expansion.ContentType))
result.Add(Expansion.ContentType, contentType);
if (allSelected || selection.Contains(Expansion.Length))
result.Add(Expansion.Length, stream.Length);
if (allSelected || selection.Contains(Expansion.Message))
result.Add(Expansion.Message, message);
if (allSelected || selection.Contains(Expansion.Text))
result.Add(Expansion.Text, text);
return result;
}
private static string ReadBinaryContent(Stream stream, out string message)
{
var size = Convert.ToInt32(stream.Length);

var buffer = new char[size];
using (var reader = new StreamReader(stream, Encoding.UTF8))
reader.ReadBlockAsync(buffer, 0, size)
.ConfigureAwait(false).GetAwaiter().GetResult();

// cut trailing zero bytes
while (size > 0 && buffer[size - 1] == (char)0)
size--;

// search non-text characters: 0x7f and c < 0x20 except common whitespaces: 0x09, 0x0A, 0x0D (\t \n \r).
for (var i = 0; i < size; i++)
{
var c = buffer[i];
if (c != (char) 127 && (c >= (char) 32 || Whitespaces.Contains(c)))
continue;
message = "Not a text file. Contains one or more non-text characters";
return null;
}

message = string.Empty;
return new string(buffer, 0, size);
}
}
}
135 changes: 135 additions & 0 deletions src/Tests/SenseNet.ODataTests/ODataChildrenTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand All @@ -13,6 +14,8 @@
using SenseNet.OData;
using SenseNet.ODataTests.Responses;
using SenseNet.Portal;
using SenseNet.Search;
using File = SenseNet.ContentRepository.File;
using Task = System.Threading.Tasks.Task;
// ReSharper disable CommentTypo
// ReSharper disable StringLiteralTypo
Expand Down Expand Up @@ -248,5 +251,137 @@ public async Task OD_GET_Children_Property_Filtered()
Assert.IsTrue(entities[0].Name.StartsWith("SF"));
}).ConfigureAwait(false);
}

[TestMethod]
public async Task OD_GET_Children_Binary_Expand_RejectedByCharacter()
{
await ODataChildrenTest(async () =>
{
var contentName = "non-text.settings";
WriteNonTextSettings(contentName, new byte[] {0x1F, 0x20});
var expectedTexts = GetTextContents();
// ACTION
var response = await ODataGetAsync(
$"/OData.svc/Root/System/Settings",
"?metadata=no&$select=Id,Name,Binary&$expand=Binary")
.ConfigureAwait(false);
// ASSERT
var entities = GetEntities(response);
var messages = entities.ToDictionary(x => x.Name,
x => ((JObject) x.AllProperties["Binary"])["Message"]?.ToString() ?? "null");
foreach (var name in messages.Keys)
{
if (name == contentName)
Assert.IsTrue(messages[contentName].StartsWith("Not a text file."));
else
Assert.AreEqual(name + ":", name + ":" + messages[name]);
}
var texts = entities.ToDictionary(x => x.Name, x => ((JObject)x.AllProperties["Binary"])["Text"]?.ToString() ?? "null");
foreach (var name in expectedTexts.Keys)
{
if (name == contentName)
Assert.IsTrue(texts[contentName] == string.Empty);
else
Assert.AreEqual(name + ":" + expectedTexts[name], name + ":" + texts[name]);
}
}).ConfigureAwait(false);
}

[TestMethod]
public async Task OD_GET_Children_Binary_Expand_EnabledByExtension()
{
await ODataChildrenTest(async () =>
{
WriteTextFileSettings(@"{ ""MaxExpandableSize"": 100000, ""Extensions"": [ ""md"", ""txt"", ""js"", ""settings"" ] }");
var expectedTexts = GetTextContents();
// ACTION
var response = await ODataGetAsync(
$"/OData.svc/Root/System/Settings",
"?metadata=no&$select=Id,Name,Binary&$expand=Binary")
.ConfigureAwait(false);
// ASSERT
var entities = GetEntities(response);
var messages = entities.ToDictionary(x => x.Name, x => ((JObject)x.AllProperties["Binary"])["Message"]?.ToString() ?? "null");
foreach (var name in messages.Keys)
Assert.AreEqual(name + ":", name + ":" + messages[name]);
var texts = entities.ToDictionary(x => x.Name, x => ((JObject)x.AllProperties["Binary"])["Text"]?.ToString() ?? "null");
foreach (var name in expectedTexts.Keys)
Assert.AreEqual(name + ":" + expectedTexts[name], name + ":" + texts[name]);
}).ConfigureAwait(false);
}
[TestMethod]
public async Task OD_GET_Children_Binary_Expand_RejectedByExtension()
{
await ODataChildrenTest(async () =>
{
WriteTextFileSettings(@"{ ""MaxExpandableSize"": 100000, ""Extensions"": [ ""md"", ""txt"", ""js"" ] }");
// ACTION
var response = await ODataGetAsync(
$"/OData.svc/Root/System/Settings",
"?metadata=no&$select=Id,Name,Binary&$expand=Binary")
.ConfigureAwait(false);
// ASSERT
var entities = GetEntities(response);
var messages = entities.ToDictionary(x => x.Name, x => ((JObject)x.AllProperties["Binary"])["Message"]?.ToString() ?? "null");
Assert.IsTrue(messages.Any(x=>x.Value.Contains("not a text file", StringComparison.OrdinalIgnoreCase)));
}).ConfigureAwait(false);
}
[TestMethod]
public async Task OD_GET_Children_Binary_Expand_RejectedBySize()
{
await ODataChildrenTest(async () =>
{
WriteTextFileSettings(@"{ ""MaxExpandableSize"": 10, ""Extensions"": [ ""md"", ""txt"", ""js"", ""settings"" ] }");
// ACTION
var response = await ODataGetAsync(
$"/OData.svc/Root/System/Settings",
"?metadata=no&$select=Id,Name,Binary&$expand=Binary")
.ConfigureAwait(false);
// ASSERT
var entities = GetEntities(response);
var messages = entities.ToDictionary(x => x.Name, x => ((JObject)x.AllProperties["Binary"])["Message"]?.ToString() ?? "null");
Assert.IsTrue(messages.Any(x => x.Value.StartsWith("Size limit exceed.")));
}).ConfigureAwait(false);
}

private void WriteNonTextSettings(string name, byte[] buffer)
{
var settings = new SenseNet.ContentRepository.Settings(Node.LoadNode(Repository.SettingsFolderPath))
{
Name = name
};
settings.Binary.ContentType = "application/octet-stream";
settings.Binary.SetStream(new MemoryStream(buffer));
settings.Save();
}

private void WriteTextFileSettings(string settingsJson)
{
var settings = new SenseNet.ContentRepository.Settings(Node.LoadNode(Repository.SettingsFolderPath))
{
Name = "TextFiles.settings"
};
settings.Binary.SetStream(RepositoryTools.GetStreamFromString(settingsJson));
settings.Save();
}
private Dictionary<string, string> GetTextContents()
{
var settingContent = Content.Load(Repository.SettingsFolderPath);
settingContent.ChildrenDefinition.EnableAutofilters = FilterStatus.Disabled;
var settingContents = settingContent.Children.ToArray();
return settingContents
.ToDictionary(
x => x.Name,
x => RepositoryTools.GetStreamString(((File)x.ContentHandler).Binary.GetStream()));
}
}
}

0 comments on commit 2368329

Please sign in to comment.