An external link added with out protocol defaults to http not https in the RTE Umbraco 13

Hello,

When a content editor adds an external link to the RTE via the link picker, www.mylink.com, the protocol http:// is automatically added, thus the link becomes http://www.mylink.com. Is there anyway to change the default protocol to https://?

I can see from the TinyMCE documentation that I need to set the link_default_protocol = https but I cannot see a way to do that in Umbraco.

Thanks :smiley:

I asked Claude to check the Umbraco 13 source code because I think I remembered that link_default_protocol does not work. It said this:

The http:// is being added by Umbraco itself, not TinyMCE. In tinymce.service.js there’s an insertLinkInEditor helper that hardcodes http:// whenever the URL starts with www., so the link_default_protocol TinyMCE setting never gets a chance to apply.

You can push TinyMCE options through Umbraco:CMS:RichTextEditor:CustomConfig in appsettings.json (e.g. "link_default_protocol": "https"), and that does flow into TinyMCE’s init config — but for the www. case specifically it won’t help, because Umbraco overrides it earlier.

Workarounds: have editors type the full https:// themselves, or raise this as an issue on the Umbraco-CMS repo to switch that hardcoded http:// to https:// (or to respect the configured link_default_protocol).

So it really appears hardcoded and triggered when a link starts with www.

Hi @charlesa-ccs

It looks like Umbraco prefixes http:// when no scheme is entered and this is done in the old Angular code for TinyMCE under Umbraco 13. The link_default_protocol option in TinyMCE is not used as Umbraco does not use the native TinyMCE link modal.

You will either need to replace the Angular code that does this or write a ContentSavingNotificationHandler that updates the URLs when the content is saved server side.

The AI suggestion is to create your own Angular service:

angular.module("umbraco.services").config([
    "$provide",
    function ($provide) {
        $provide.decorator("tinyMceService", [
            "$delegate",
            function ($delegate) {
                var original = $delegate.insertLinkInEditor;
                $delegate.insertLinkInEditor = function (editor, target, anchorElm) {
                    if (target && target.url && typeof target.url === "string") {
                        var url = target.url;
                        var isAbsolute = /^[a-z][a-z0-9+\-.]*:\/\//i.test(url);
                        var isRelative = url.startsWith("/") || url.startsWith("#")
                                       || url.startsWith("mailto:") || url.startsWith("tel:");
                        if (!isAbsolute && !isRelative) {
                            target.url = "https://" + url;
                        }
                    }
                    return original.apply(this, arguments);
                };
                return $delegate;
            }
        ]);
    }
]);

and register it in your package.manifest file.

{
  "javascript": [
    "~/App_Plugins/YourPackage/yourplugin.js"
  ]
}

I’ve not tested this though.

Justin

2 Likes

Hi @charlesa-ccs ,

Sharing one more way to do it.

You can also create a HtmlHelper Extension:
Install the HtmlAgilityPack first as this is going to use that.
Create a static class in your project. This will take the raw HTML from the Rich Text Editor and parse it safely before writing it to the browser.

using HtmlAgilityPack;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace RteTestProject.Extensions
{
    public static class HtmlHelperExtensions
    {
        public static IHtmlContent SecureRteLinks(this IHtmlHelper htmlHelper, string rawHtml)
        {
            if (string.IsNullOrWhiteSpace(rawHtml))
                return new HtmlString(string.Empty);

            var doc = new HtmlDocument();
            doc.LoadHtml(rawHtml);

            // Find all anchor tags that contain an href attribute
            var links = doc.DocumentNode.SelectNodes("//a[@href]");

            if (links != null)
            {
                foreach (var link in links)
                {
                    var href = link.GetAttributeValue("href", string.Empty);

                    // Check if it starts with http:// and replace it
                    if (href.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
                    {
                        var secureHref = "https://" + href.Substring(7);
                        link.SetAttributeValue("href", secureHref);
                    }
                }
            }

            return new HtmlString(doc.DocumentNode.OuterHtml);
        }
    }
}

How to use it in your Views:
In your Razor views, instead of rendering the Rich Text Editor property normally, pass it through your new extension.

@using Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.Test>
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@using RteTestProject.Extensions;
@{
    Layout = null;
    // Get the raw HTML string from the property
    var rawRteContent = Model.Value<string>("bodyText");
}

<!DOCTYPE html>
<html>
<head>
    <title>@Model.Name</title>
</head>
<body>
    
    <h1>Test Output</h1>

    <!-- Pass the raw string through the parser -->
    <div>
        @Html.SecureRteLinks(rawRteContent)
    </div>

</body>
</html>

I have also did a quick test as well, here are the findings:

Hope it helps!

1 Like

Thanks Luuk

Thanks Justin I will take a look

Thanks @ShekharTarare was trying to not have to edit the links, but I maybe I will need to do it this way. I will give it a look :smiley:

1 Like

To be fair to Umbraco, for external links, Umbraco has no way of knowing if the link should be http or https, so in that sense I would much rather teach the editors to always include the schema and have Umbraco validate the URL if it’s incomplete.

But, I agree that these days, it’s much more probable that it’s https, so IF you’re autmatically prepending, it should be https and not http.

Also, most sites do automatic redirects from http:// to https:// so it’s bad practice if they’ve not done that. I appreciate that’s out of your control but maybe worth raising with them (unless it’s not specific sites and is too many random URLs!).

One more thought.. so you don’t have to revisit content.. is to override the PropertyValueConvertor and update instances of http:// to https:// when frontend calls for the rendering.
(that way database still holds the incorrect http:// but we’ve corrected it at the point of use, and universal across all rtes)

here’s a v17 override, that’s removing empty trailing

, though for v13 I think it’s probably
Umbraco-CMS/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs at release-13.14.0 · umbraco/Umbraco-CMS
or maybe…
Umbraco-CMS/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs at release-13.14.0 · umbraco/Umbraco-CMS

using HtmlAgilityPack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Blocks;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Templates;

namespace Lvl.Code.Extensions.PropertyValueConvertors;

// We inherit from the core class so we satisfy the "IsConverter" check
public class CustomRtePropertyValueConverter : RteBlockRenderingValueConverter
{
    public CustomRtePropertyValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser, IPartialViewBlockEngine partialViewBlockEngine, BlockEditorConverter blockEditorConverter, IJsonSerializer jsonSerializer, IApiElementBuilder apiElementBuilder, RichTextBlockPropertyValueConstructorCache constructorCache, ILogger<CustomRtePropertyValueConverter> logger, IVariationContextAccessor variationContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler, IOptionsMonitor<DeliveryApiSettings> deliveryApiSettingsMonitor) : base(linkParser, urlParser, imageSourceParser, apiRichTextElementParser, apiRichTextMarkupParser, partialViewBlockEngine, blockEditorConverter, jsonSerializer, apiElementBuilder, constructorCache, logger, variationContextAccessor, blockEditorVarianceHandler, deliveryApiSettingsMonitor)
    {
    }

    public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
    {
        // 1. Let the base class do ALL the heavy lifting (parsing links, rendering blocks, etc.)
        var baseResult = base.ConvertIntermediateToObject(owner, propertyType, referenceCacheLevel, inter, preview);

        if (baseResult is not IHtmlEncodedString htmlEncoded)
        {
            return new HtmlEncodedString(string.Empty);
        }

        var rawHtml = htmlEncoded.ToHtmlString();
        if (string.IsNullOrWhiteSpace(rawHtml))
        {
            return new HtmlEncodedString(string.Empty);
        }

        // 2. Perform your surgical strike on the finished HTML string
        var htmlDoc = new HtmlDocument();
        htmlDoc.LoadHtml(rawHtml);

        bool modified = false;
        while (htmlDoc.DocumentNode.LastChild != null && IsNodeEmpty(htmlDoc.DocumentNode.LastChild))
        {
            htmlDoc.DocumentNode.LastChild.Remove();
            modified = true;
        }

        return modified
            ? new HtmlEncodedString(htmlDoc.DocumentNode.OuterHtml)
            : htmlEncoded;
    }

    private static bool IsNodeEmpty(HtmlNode node)
    {
        if (node.Name is "p" or "br")
        {
            var text = node.InnerText.Replace("&nbsp;", "").Trim();
            return string.IsNullOrWhiteSpace(text);
        }
        return node.NodeType == HtmlNodeType.Text && string.IsNullOrWhiteSpace(node.InnerText);
    }

    public class RteConverterComposer : IComposer
    {
        public void Compose(IUmbracoBuilder builder)
        {
            // 1. Remove the standard RTE converter (the "enhanced" one)
            builder.PropertyValueConverters().Remove<RteBlockRenderingValueConverter>();

            // 2. Just in case, remove the simple one (though Umbraco likely already did)
            builder.PropertyValueConverters().Remove<SimpleRichTextValueConverter>();

            // 3. Add yours to the end of the collection
            builder.PropertyValueConverters().Append<CustomRtePropertyValueConverter>();
        }
    }
}