diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index e61bd38bc0..9ac2635489 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -156,6 +156,55 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } } + function uploadImageHandler(blobInfo, success, failure, progress){ + + //TODO: Worth refactoring to Angular $http calls + //Rather than XHR?? + + let xhr, formData; + + xhr = new XMLHttpRequest(); + xhr.open('POST', Umbraco.Sys.ServerVariables.umbracoUrls.tinyMceApiBaseUrl + 'UploadImage'); + + xhr.upload.onprogress = function (e) { + progress(e.loaded / e.total * 100); + }; + + xhr.onerror = function () { + failure('Image upload failed due to a XHR Transport error. Code: ' + xhr.status); + }; + + xhr.onload = function () { + let json; + + if (xhr.status < 200 || xhr.status >= 300) { + failure('HTTP Error: ' + xhr.status); + return; + } + + json = JSON.parse(xhr.responseText); + + if (!json || typeof json.location !== 'string') { + failure('Invalid JSON: ' + xhr.responseText); + return; + } + + // TODO: The JSON contains location & udi + // Need to add UDI as attr + + success(json.location); + }; + + formData = new FormData(); + formData.append('file', blobInfo.blob(), blobInfo.blob().name); + + //TODO: Send Media Parent ID from config + //TODO: How will each RTE pass in this value to this function?! + formData.append('mediaParent', -1); + + xhr.send(formData); + } + return { /** @@ -237,13 +286,19 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s insert_toolbar: toolbars.insertToolbar, selection_toolbar: toolbars.selectionToolbar, - body_class: 'umb-rte', + body_class: "umb-rte", //see http://archive.tinymce.com/wiki.php/Configuration:cache_suffix cache_suffix: "?umb__rnd=" + Umbraco.Sys.ServerVariables.application.cacheBuster, //this is used to style the inline macro bits, sorry hard coding this form now since we don't have a standalone //stylesheet to load in for this with only these styles (the color is @pinkLight) content_style: ".mce-content-body .umb-macro-holder { border: 3px dotted #f5c1bc; padding: 7px; display: block; margin: 3px; } .umb-rte .mce-content-body .umb-macro-holder.loading {background: url(assets/img/loader.gif) right no-repeat; background-size: 18px; background-position-x: 99%;}" + + // This allows images to be pasted in & stored as Base64 until they get uploaded to server + paste_data_images: true + + images_upload_handler: uploadImageHandler, + automatic_uploads: false, }; if (tinyMceConfig.customConfig) { @@ -281,7 +336,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s angular.extend(config, tinyMceConfig.customConfig); } - return $q.when(config); }); @@ -428,7 +482,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s id: '__mcenew', 'data-udi': img.udi }; - + editor.selection.setContent(editor.dom.createHTML('img', data)); $timeout(function () { @@ -447,9 +501,9 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } } editor.dom.setAttrib(imgElm, 'id', null); - + editor.fire('Change'); - + }, 500); } }, @@ -521,7 +575,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s onPostRender: function () { let ctrl = this; - + /** * Check if the macro is currently selected and toggle the menu button */ @@ -1134,7 +1188,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }; editorService.linkPicker(linkPicker); }); - + }); //Create the insert media plugin diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 58f49f4261..dc46a97df4 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -309,7 +309,11 @@ namespace Umbraco.Web.Editors { "webProfilingBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetStatus()) - } + }, + { + "tinyMceApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.UploadImage()) + }, } }, { diff --git a/src/Umbraco.Web/Editors/TinyMceController.cs b/src/Umbraco.Web/Editors/TinyMceController.cs new file mode 100644 index 0000000000..06b827568a --- /dev/null +++ b/src/Umbraco.Web/Editors/TinyMceController.cs @@ -0,0 +1,107 @@ +using System.Net; +using System.Net.Http; +using System.Web; +using System.Web.Http; +using Umbraco.Core.Services; +using Umbraco.Web.WebApi; +using Constants = Umbraco.Core.Constants; +using Umbraco.Core; +using Umbraco.Web.Mvc; +using Umbraco.Core.IO; +using System.IO; +using System.Threading.Tasks; +using Umbraco.Web.Composing; +using Umbraco.Core.Configuration.UmbracoSettings; +using System.Linq; +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.Editors +{ + [PluginController("UmbracoApi")] + public class TinyMceController : UmbracoAuthorizedApiController + { + private IMediaService _mediaService; + private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + + public TinyMceController(IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) + { + _mediaService = mediaService; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + } + + [HttpPost] + public async Task UploadImage() + { + + if (Request.Content.IsMimeMultipartContent() == false) + { + throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); + } + + var root = IOHelper.MapPath(SystemDirectories.TempFileUploads); + + // Ensure it exists + Directory.CreateDirectory(root); + var provider = new MultipartFormDataStreamProvider(root); + + var result = await Request.Content.ReadAsMultipartAsync(provider); + + // Must have a file + if (result.FileData.Count == 0) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + // Should only have one file + if (result.FileData.Count > 1) + { + return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Only one file can be uploaded at a time"); + } + + // Check we have mediaParent as posted data + if (string.IsNullOrEmpty(result.FormData["mediaParent"])) + { + return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Missing the Media Parent folder to save this image"); + } + + // Really we should only have one file per request to this endpoint + var file = result.FileData[0]; + + var parentFolder = Convert.ToInt32(result.FormData["mediaParent"]); + + var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }).TrimEnd(); + var safeFileName = fileName.ToSafeFileName(); + var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); + + if (Current.Configs.Settings().Content.IsFileAllowedForUpload(ext) == false || Current.Configs.Settings().Content.ImageFileTypes.Contains(ext) == false) + { + // Throw some error - to say can't upload this IMG type + return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "This is not an image filetype extension that is approved"); + } + + var mediaItemName = fileName.ToFriendlyName(); + var f = _mediaService.CreateMedia(mediaItemName, parentFolder, Constants.Conventions.MediaTypes.Image, Security.CurrentUser.Id); + var fileInfo = new FileInfo(file.LocalFileName); + var fs = fileInfo.OpenReadWithRetry(); + if (fs == null) throw new InvalidOperationException("Could not acquire file stream"); + using (fs) + { + f.SetValue(Services.ContentTypeBaseServices, Constants.Conventions.Media.File, fileName, fs); + } + + _mediaService.Save(f, Security.CurrentUser.Id); + + // Need to get URL to the media item and its UDI + var udi = f.GetUdi(); + + + // TODO: Check this is the BEST way to get the URL + // Ensuring if they use some CDN & blob storage?! + var mediaTyped = Umbraco.Media(f.Id); + var location = mediaTyped.Url; + + return Request.CreateResponse(HttpStatusCode.OK, new { location = location, udi = udi }); + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 222d58d4dc..f696178a86 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -148,6 +148,7 @@ +