Article Handling Embedded Files in Content

When creating custom content types to the Verint Community platform and utilizing the platform editor within the UI managing that content (using the $core_v2_editor widget API), the editor will allow drag-and-drop and menu-based file uploading into the HTML content if that content supports embedded files.

Why Would I Want to Enable Embedded Files?

Embedded files are useful when authoring rich formatted content (HTML) to allow inclusion of images, videos, documents, and other media and files to provide more flexibility and interactivity. 

Implementing Support for Embedded Files

To add support for embedded files within a custom content type, the IFileEmbeddableContentType plugin type should also be implemented on your content type (which requires a reference to Telligent.Evolution.Core.dll). The IFileEmbeddableContentType interface extends IContentType to add members that enable the content type to interact with the Verint Community platform regarding embedded files.

Basic Implementation

Because an implementation of IFileEmbeddableContentType is dependent on a custom content type which is out-of-scope for this topic, I'll show examples from private messaging in the core platform.

We'll implement the IFileEmbeddedContentType, which requires two members to be implemented. First, CanAddFiles() identifies whether a user can add files to this content type:

public bool CanAddFiles(int userId)
{
	var user = Users.GetUser(userId);
	if (user == null || user.IsAnonymous)
		return false;

	return true;
}

For private messaging, any non-anonymous user can embed files. For other applications, the ability to embed files may be dependent on the application or container/group the content is being created within. To detect the context of the request, the current page's context can be retrieved by analyzing Url.CurrentContext or using another mechanism for detecting the context of the CanAddFiles() request.

When CanAddFiles() returns true, file embedding functionality within the platform content editor will be enabled, otherwise, these functions will not be available.

The only remaining requirement for an IFileEmbeddableContentType implementation is the SetController() method:

private IFileEmbeddableContentTypeController _embeddedFileController;

public void SetController(IFileEmbeddableContentTypeController controller)
{
	_embeddedFileController = controller;
}

This method provides the plugin with a controller enabling the extraction and reassignment of embedded files within HTML content.

Using the Controller to Process Files

When files are uploaded through the editor, they're placed in a temporary file store within the Centralized File System. Files placed in this temporary location are only accessible to the user who uploaded them and are automatically deleted after 2 hours (by default) of inactivity. Content supporting embedded files should move this temporary content to a final storage location suitably secured for the content type. To implement the movement of files to a final storage location, events related to the creation or editing of content should be handled and the IFileEmbeddableContentTypeController provided to the plugin through the SetController() method should be used.

For private messaging, we attach handlers to the BeforeCreate event (since private messages do not support editing, otherwise, we'd also want to handle edit-events). As with all plugins, we register the event handler in the Initialize() method of IPlugin:

public void Initialize()
{
    var conversationMessageApi = Telligent.Evolution.Extensibility.Apis.Get<Telligent.Evolution.Extensibility.Api.Version1.IConversationMessages>();
	
	conversationMessageApi.Events.BeforeCreate += ConversationMessage_BeforeCreate;
    
    // other event handlers used for this plugin
}

void ConversationMessage_BeforeCreate(ConversationMessageBeforeCreateEventArgs e)
{
    if (_embeddedFileController == null)
		return;

	var hasPermission = CanAddFiles(e.Author.Id.Value);
	var targetFileStore = Telligent.Evolution.Extensibility.Storage.Version1.CentralizedFileStorage.GetFileStore("conversationfiles");

	e.Body = _embeddedFileController.SaveFilesInHtml(e.Body, f => {
		if (!hasPermission)
			_embeddedFileController.InvalidFile(f, "You do not have permission to add files to private messages");

		string message;
		if (!IsFileValid(e.Author.Id.Value, f.FileName, f.ContentLength, true, out message))
			EmbeddedFileController.InvalidFile(f, message);

		using (var stream = f.OpenReadStream())
		{
			var targetFile = targetFileStore.AddFile(e.ConversationId.Value.ToString("N"), f.FileName, stream, true);
			if (targetFile != null)
				return targetFile;
			else
				_embeddedFileController.InvalidFile(f, "An error occurred while saving an embedded file.");
		}

		return null;
	});
}

In the event handler, ConversationMessage_BeforeCreate, we retrieve the target CFS file store, conversationfiles, using the CentralizedFileStorage API -- this is where valid files will be moved. Then, we use the controller's SaveFilesInHtml() method to process each file found within the body (e.Body) of the private message. If we wanted to support embedding files in other properties of the message (in this case, there are none), we would call SaveFilesInHtml() for each property.

SaveFilesInHtml() calls the function defined by the last parameter to the method call for each temporary file in the provided content (in this case, e.Body). For each file, we'll verify the user has permission to save files and validate the file type (via the IsFileValid() method which would be defined in this plugin as well but is out-of-scope for this discussion -- it reviews the file details and returns false and sets the out message parameter if a the file is invalid). If there is an issue with any file (one of the validation procedures fails), we call InvalidFile() on the controller. This stops processing and reports the provided message back to the user. In production code (this sample is a simplification), we'd want to ensure that this text is properly translated.

After validating the file is valid, we'll move it to the proper target file store by calling AddFile() on the target CFS file store. This will return the resulting ICentralizedFile which then must be provided back to the controller.

SaveFileInHtml() returns the updated HTML with all temporary embedded files appropriately moved. This value should be saved back to the content. For private messages, we set e.Body to the new value and, because this is in the BeforeCreate event, the updates will be committed normally (if we processed content in AfterCreate, for example, the content would have already been committed and we'd need to explicitly save the updated value).

UI Considerations

To ensure that the content editor is aware of the type of content being edited and to enable the editor to detect if file embedding is supported, it is important that when rendering the editor through $core_v2_editor.Render() in a widget, that the ContentTypeId is specified. This identifier should match the ContentTypeId identified by the IContentType implementation of the IFileEmbeddableContentType. Without this option specified, the editor will not know what type of content is being edited and will never enable file embedding.