Merge pull request #5209 from umbraco/v8/feature/4940-save-media-reference-instead-of-media-url

Save media reference instead of media url
This commit is contained in:
Shannon Deminick
2019-05-07 16:33:30 +10:00
committed by GitHub
12 changed files with 257 additions and 43 deletions

View File

@@ -6,6 +6,7 @@ using Umbraco.Core.Migrations.Upgrade.V_7_12_0;
using Umbraco.Core.Migrations.Upgrade.V_7_14_0;
using Umbraco.Core.Migrations.Upgrade.V_8_0_0;
using Umbraco.Core.Migrations.Upgrade.V_8_0_1;
using Umbraco.Core.Migrations.Upgrade.V_8_1_0;
namespace Umbraco.Core.Migrations.Upgrade
{
@@ -139,6 +140,7 @@ namespace Umbraco.Core.Migrations.Upgrade
To<RenameLabelAndRichTextPropertyEditorAliases>("{E0CBE54D-A84F-4A8F-9B13-900945FD7ED9}");
To<MergeDateAndDateTimePropertyEditor>("{78BAF571-90D0-4D28-8175-EF96316DA789}");
To<ChangeNuCacheJsonFormat>("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}");
To<ConvertTinyMceAndGridMediaUrlsToLocalLink>("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}");
//FINAL

View File

@@ -0,0 +1,90 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Migrations.PostMigrations;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Services;
namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0
{
public class ConvertTinyMceAndGridMediaUrlsToLocalLink : MigrationBase
{
private readonly IMediaService _mediaService;
public ConvertTinyMceAndGridMediaUrlsToLocalLink(IMigrationContext context, IMediaService mediaService) : base(context)
{
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
}
public override void Migrate()
{
var mediaLinkPattern = new Regex(
@"(<a[^>]*href="")(\/ media[^""\?]*)([^>]*>)",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
var sqlPropertyData = Sql()
.Select<PropertyDataDto>()
.AndSelect<PropertyTypeDto>()
.AndSelect<DataTypeDto>()
.From<PropertyDataDto>()
.InnerJoin<PropertyTypeDto>().On<PropertyDataDto, PropertyTypeDto>((left, right) => left.PropertyTypeId == right.Id)
.InnerJoin<DataTypeDto>().On<PropertyTypeDto, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId)
.Where<DataTypeDto>(x =>
x.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce ||
x.EditorAlias == Constants.PropertyEditors.Aliases.Grid);
var properties = Database.Fetch<PropertyDataDto>(sqlPropertyData);
foreach (var property in properties)
{
var value = property.TextValue;
if (string.IsNullOrWhiteSpace(value)) continue;
if (property.PropertyTypeDto.DataTypeDto.EditorAlias == Constants.PropertyEditors.Aliases.Grid)
{
var obj = JsonConvert.DeserializeObject<JObject>(value);
var allControls = obj.SelectTokens("$.sections..rows..areas..controls");
foreach (var control in allControls.SelectMany(c => c))
{
var controlValue = control["value"];
if (controlValue.Type == JTokenType.String)
{
control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value<string>());
}
}
property.TextValue = JsonConvert.SerializeObject(obj);
}
else
{
property.TextValue = UpdateMediaUrls(mediaLinkPattern, value);
}
Database.Update(property);
}
Context.AddPostMigration<RebuildPublishedSnapshot>();
}
private string UpdateMediaUrls(Regex mediaLinkPattern, string value)
{
return mediaLinkPattern.Replace(value, match =>
{
// match groups:
// - 1 = from the beginning of the a tag until href attribute value begins
// - 2 = the href attribute value excluding the querystring (if present)
// - 3 = anything after group 2 until the a tag is closed
var href = match.Groups[2].Value;
var media = _mediaService.GetMediaByPath(href);
return media == null
? match.Value
: $"{match.Groups[1].Value}/{{localLink:{media.GetUdi()}}}{match.Groups[3].Value}";
});
}
}
}

View File

@@ -219,6 +219,7 @@
<Compile Include="Mapping\MapDefinitionCollectionBuilder.cs" />
<Compile Include="Mapping\IMapDefinition.cs" />
<Compile Include="Mapping\UmbracoMapper.cs" />
<Compile Include="Migrations\Upgrade\V_8_1_0\ConvertTinyMceAndGridMediaUrlsToLocalLink.cs" />
<Compile Include="Models\CultureImpact.cs" />
<Compile Include="Models\PublishedContent\ILivePublishedModelFactory.cs" />
<Compile Include="Persistence\Dtos\PropertyTypeCommonDto.cs" />

View File

@@ -61,10 +61,12 @@ namespace Umbraco.Tests.Web
[TestCase("", "")]
[TestCase("hello href=\"{localLink:1234}\" world ", "hello href=\"/my-test-url\" world ")]
[TestCase("hello href=\"{localLink:umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")]
[TestCase("hello href=\"{localLink:umb://document-type/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")]
[TestCase("hello href=\"{localLink:umb://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")]
[TestCase("hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")]
[TestCase("hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/1001/my-image.jpg\" world ")]
//this one has an invalid char so won't match
[TestCase("hello href=\"{localLink:umb^://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"{localLink:umb^://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ")]
[TestCase("hello href=\"{localLink:umb^://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"{localLink:umb^://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ")]
[TestCase("hello href=\"{localLink:umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"#\" world ")]
public void ParseLocalLinks(string input, string result)
{
var serviceCtxMock = new TestObjects(null).GetServiceContextMock();
@@ -77,7 +79,7 @@ namespace Umbraco.Tests.Web
// return Attempt.Succeed(1234);
// });
//setup a mock url provider which we'll use fo rtesting
//setup a mock url provider which we'll use for testing
var testUrlProvider = new Mock<IUrlProvider>();
testUrlProvider
.Setup(x => x.GetUrl(It.IsAny<UmbracoContext>(), It.IsAny<IPublishedContent>(), It.IsAny<UrlProviderMode>(), It.IsAny<string>(), It.IsAny<Uri>()))
@@ -96,6 +98,10 @@ namespace Umbraco.Tests.Web
Mock.Get(snapshot).Setup(x => x.Content).Returns(contentCache);
var snapshotService = Mock.Of<IPublishedSnapshotService>();
Mock.Get(snapshotService).Setup(x => x.CreatePublishedSnapshot(It.IsAny<string>())).Returns(snapshot);
var media = Mock.Of<IPublishedContent>();
Mock.Get(media).Setup(x => x.Url).Returns("/media/1001/my-image.jpg");
var mediaCache = Mock.Of<IPublishedMediaCache>();
Mock.Get(mediaCache).Setup(x => x.GetById(It.IsAny<Guid>())).Returns(media);
var umbracoContextFactory = new UmbracoContextFactory(
Umbraco.Web.Composing.Current.UmbracoContextAccessor,
@@ -110,7 +116,7 @@ namespace Umbraco.Tests.Web
using (var reference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of<HttpContextBase>()))
{
var output = TemplateUtilities.ParseInternalLinks(input, reference.UmbracoContext.UrlProvider);
var output = TemplateUtilities.ParseInternalLinks(input, reference.UmbracoContext.UrlProvider, mediaCache);
Assert.AreEqual(result, output);
}

View File

@@ -431,6 +431,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
} else {
//Considering these fixed because UDI will now be used and thus
// we have no need for rel http://issues.umbraco.org/issue/U4-6228, http://issues.umbraco.org/issue/U4-6595
//TODO: Kill rel attribute
data["rel"] = img.id;
data["data-id"] = img.id;
}
@@ -964,12 +965,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
rel: target.rel ? target.rel : null
};
if (hasUdi) {
a["data-udi"] = target.udi;
} else if (target.id) {
a["data-id"] = target.id;
}
if (target.anchor) {
a["data-anchor"] = target.anchor;
a.href = a.href + target.anchor;
@@ -996,8 +991,8 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
return;
}
//if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set
if (id && (angular.isUndefined(target.isMedia) || !target.isMedia)) {
//if we have an id, it must be a locallink:id
if (id) {
href = "/{localLink:" + id + "}";

View File

@@ -0,0 +1,47 @@
(function () {
"use strict";
/**
* @ngdoc service
* @name umbraco.services.udiParser
* @description A object used to parse UDIs
**/
function udiParser() {
return {
/**
* @ngdoc method
* @name umbraco.services.udiParser#parse
* @methodOf umbraco.services.udiParser
* @function
*
* @description
* Converts the string representation of an entity identifier into the equivalent Udi instance.
*
* @param {string} input The string to parse
* @returns {Object} The parsed UDI or null if input isn't a valid UDI
*/
parse: function(input) {
if (!input || typeof input !== "string" || !input.startsWith("umb://"))
return null;
var lastIndexOfSlash = input.substring("umb://".length).lastIndexOf("/");
var entityType = lastIndexOfSlash === -1 ? input.substring("umb://".length) : input.substr("umb://".length, lastIndexOfSlash);
var value = lastIndexOfSlash === -1 ? null : input.substring("umb://".length + lastIndexOfSlash + 1);
return {
entityType,
value,
toString: function() {
return "umb://" + entityType + (value === null ? "" : "/" + value);
}
}
}
}
}
angular.module("umbraco.services").factory("udiParser", udiParser);
})();

View File

@@ -1,6 +1,6 @@
//used for the media picker dialog
angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
function ($scope, eventsService, entityResource, contentResource, mediaHelper, userService, localizationService, tinyMceService, editorService) {
function ($scope, eventsService, entityResource, contentResource, mediaResource, mediaHelper, udiParser, userService, localizationService, tinyMceService, editorService) {
var vm = this;
var dialogOptions = $scope.model;
@@ -66,8 +66,22 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
//will be either a udi or an int
var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id;
// is it a content link?
if (!$scope.model.target.isMedia) {
if ($scope.model.target.udi) {
// extract the entity type from the udi and set target.isMedia accordingly
var udi = udiParser.parse(id);
if (udi && udi.entityType === "media") {
$scope.model.target.isMedia = true;
} else {
delete $scope.model.target.isMedia;
}
}
if ($scope.model.target.isMedia) {
mediaResource.getById(id).then(function (resp) {
$scope.model.target.url = resp.mediaLink;
});
} else {
// get the content path
entityResource.getPath(id, "Document").then(function (path) {
$scope.model.target.path = path;
@@ -96,9 +110,9 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
}
}
// need to translate the link target ("_blank" or "") into a boolean value for umb-checkbox
// need to translate the link target ("_blank" or "") into a boolean value for umb-checkbox
vm.openInNewWindow = $scope.model.target.target === "_blank";
}
}
else if (dialogOptions.anchors) {
$scope.anchorValues = dialogOptions.anchors;
}

View File

@@ -64,8 +64,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en
var target = link ? {
name: link.name,
anchor: link.queryString,
// the linkPicker breaks if it get an udi for media
udi: link.isMedia ? null : link.udi,
udi: link.udi,
url: link.url,
target: link.target
} : null;
@@ -80,21 +79,13 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en
model.target.anchor = (model.target.anchor.indexOf('=') === -1 ? '#' : '?') + model.target.anchor;
}
if (link) {
if (link.isMedia && link.url === model.target.url) {
// we can assume the existing media item is changed and no new file has been selected
// so we don't need to update the udi and isMedia fields
} else {
link.udi = model.target.udi;
link.isMedia = model.target.isMedia;
}
link.udi = model.target.udi;
link.name = model.target.name || model.target.url || model.target.anchor;
link.queryString = model.target.anchor;
link.target = model.target.target;
link.url = model.target.url;
} else {
link = {
isMedia: model.target.isMedia,
name: model.target.name || model.target.url || model.target.anchor,
queryString: model.target.anchor,
target: model.target.target,
@@ -105,7 +96,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en
}
if (link.udi) {
var entityType = link.isMedia ? "media" : "document";
var entityType = model.target.isMedia ? "Media" : "Document";
entityResource.getById(link.udi, entityType).then(function (data) {
link.icon = iconHelper.convertFromLegacyIcon(data.icon);

View File

@@ -0,0 +1,61 @@
describe("udiParser tests", function () {
var udiParser;
beforeEach(module('umbraco.services'));
beforeEach(inject(function ($injector) {
udiParser = $injector.get('udiParser');
}));
describe("Parse UDI", function () {
it("can parse a valid document UDI", function() {
var entityType = "document";
var key = "c0a62ced-6402-4025-8d46-a234a34f6a56";
var value = "umb://" + entityType + "/" + key;
var udi = udiParser.parse(value);
expect(udi.entityType).toBe(entityType);
expect(udi.value).toBe(key);
});
it("can parse a valid media UDI", function() {
var entityType = "media";
var key = "f82f3313-f8e9-42b0-a67f-80a7f452dd21";
var value = "umb://" + entityType + "/" + key;
var udi = udiParser.parse(value);
expect(udi.entityType).toBe(entityType);
expect(udi.value).toBe(key);
});
it("returns the full UDI when calling toString() on the returned value", function() {
var value = "umb://document/c0a62ced-6402-4025-8d46-a234a34f6a56";
var udi = udiParser.parse(value);
expect(udi.toString()).toBe(value);
});
it("can parse an open UDI", function() {
var entityType = "media";
var value = "umb://" + entityType;
var udi = udiParser.parse(value);
expect(udi.entityType).toBe(entityType);
expect(udi.value).toBeNull();
expect(udi.toString()).toBe(value);
});
it("returns null if the input is invalid", function() {
var value = "not an UDI";
var udi = udiParser.parse(value);
expect(udi).toBeNull();
});
});
});

View File

@@ -9,9 +9,6 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "icon")]
public string Icon { get; set; }
[DataMember(Name = "isMedia")]
public bool IsMedia { get; set; }
[DataMember(Name = "name")]
public string Name { get; set; }

View File

@@ -64,7 +64,6 @@ namespace Umbraco.Web.PropertyEditors
{
GuidUdi udi = null;
var icon = "icon-link";
var isMedia = false;
var published = true;
var trashed = false;
var url = dto.Url;
@@ -88,7 +87,6 @@ namespace Umbraco.Web.PropertyEditors
else if(entity is IContentEntitySlim contentEntity)
{
icon = contentEntity.ContentTypeIcon;
isMedia = true;
published = !contentEntity.Trashed;
udi = new GuidUdi(Constants.UdiEntityType.Media, contentEntity.Key);
url = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(entity.Key)?.Url ?? "#";
@@ -104,7 +102,6 @@ namespace Umbraco.Web.PropertyEditors
result.Add(new LinkDisplay
{
Icon = icon,
IsMedia = isMedia,
Name = dto.Name,
Target = dto.Target,
Trashed = trashed,

View File

@@ -4,6 +4,7 @@ using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Web.Composing;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
namespace Umbraco.Web.Templates
@@ -32,9 +33,14 @@ namespace Umbraco.Web.Templates
/// <param name="text"></param>
/// <param name="urlProvider"></param>
/// <returns></returns>
public static string ParseInternalLinks(string text, UrlProvider urlProvider)
public static string ParseInternalLinks(string text, UrlProvider urlProvider) =>
ParseInternalLinks(text, urlProvider, Current.UmbracoContext.MediaCache);
// TODO: Replace mediaCache with media url provider
internal static string ParseInternalLinks(string text, UrlProvider urlProvider, IPublishedMediaCache mediaCache)
{
if (urlProvider == null) throw new ArgumentNullException("urlProvider");
if (urlProvider == null) throw new ArgumentNullException(nameof(urlProvider));
if (mediaCache == null) throw new ArgumentNullException(nameof(mediaCache));
// Parse internal links
var tags = LocalLinkPattern.Matches(text);
@@ -45,18 +51,25 @@ namespace Umbraco.Web.Templates
var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1);
//The id could be an int or a UDI
Udi udi;
if (Udi.TryParse(id, out udi))
if (Udi.TryParse(id, out var udi))
{
var guidUdi = udi as GuidUdi;
if (guidUdi != null)
{
var newLink = urlProvider.GetUrl(guidUdi.Guid);
var newLink = "#";
if (guidUdi.EntityType == Constants.UdiEntityType.Document)
newLink = urlProvider.GetUrl(guidUdi.Guid);
else if (guidUdi.EntityType == Constants.UdiEntityType.Media)
newLink = mediaCache.GetById(guidUdi.Guid)?.Url;
if (newLink == null)
newLink = "#";
text = text.Replace(tag.Value, "href=\"" + newLink);
}
}
int intId;
if (int.TryParse(id, out intId))
if (int.TryParse(id, out var intId))
{
var newLink = urlProvider.GetUrl(intId);
text = text.Replace(tag.Value, "href=\"" + newLink);