using System; using System.Collections.Generic; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Umbraco.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Extensions; using Umbraco.ModelsBuilder.Embedded.BackOffice; using Umbraco.Web.Common.Lifetime; using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.WebAssets; namespace Umbraco.ModelsBuilder.Embedded { /// /// Handles and notifications to initialize MB /// internal class ModelsBuilderNotificationHandler : INotificationHandler, INotificationHandler { private readonly ModelsBuilderSettings _config; private readonly IShortStringHelper _shortStringHelper; private readonly LiveModelsProvider _liveModelsProvider; private readonly OutOfDateModelsStatus _outOfDateModels; private readonly LinkGenerator _linkGenerator; private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; private readonly ContentModelBinder _modelBinder; public ModelsBuilderNotificationHandler( IOptions config, IShortStringHelper shortStringHelper, LiveModelsProvider liveModelsProvider, OutOfDateModelsStatus outOfDateModels, LinkGenerator linkGenerator, IUmbracoRequestLifetime umbracoRequestLifetime, ContentModelBinder modelBinder) { _config = config.Value; _shortStringHelper = shortStringHelper; _liveModelsProvider = liveModelsProvider; _outOfDateModels = outOfDateModels; _shortStringHelper = shortStringHelper; _linkGenerator = linkGenerator; _umbracoRequestLifetime = umbracoRequestLifetime; _modelBinder = modelBinder; } /// /// Handles the notification /// public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken) { // always setup the dashboard // note: UmbracoApiController instances are automatically registered _umbracoRequestLifetime.RequestEnd += (sender, context) => _liveModelsProvider.AppEndRequest(context); _modelBinder.ModelBindingException += ContentModelBinder_ModelBindingException; if (_config.Enable) { FileService.SavingTemplate += FileService_SavingTemplate; } if (_config.ModelsMode.IsLiveNotPure()) { _liveModelsProvider.Install(); } if (_config.FlagOutOfDateModels) { _outOfDateModels.Install(); } return Task.CompletedTask; } /// /// Handles the notification /// public Task HandleAsync(ServerVariablesParsing notification, CancellationToken cancellationToken) { var serverVars = notification.ServerVariables; if (!serverVars.ContainsKey("umbracoUrls")) { throw new ArgumentException("Missing umbracoUrls."); } var umbracoUrlsObject = serverVars["umbracoUrls"]; if (umbracoUrlsObject == null) { throw new ArgumentException("Null umbracoUrls"); } if (!(umbracoUrlsObject is Dictionary umbracoUrls)) { throw new ArgumentException("Invalid umbracoUrls"); } if (!serverVars.ContainsKey("umbracoPlugins")) { throw new ArgumentException("Missing umbracoPlugins."); } if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) { throw new ArgumentException("Invalid umbracoPlugins"); } umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); return Task.CompletedTask; } private Dictionary GetModelsBuilderSettings() { var settings = new Dictionary { {"enabled", _config.Enable} }; return settings; } /// /// Used to check if a template is being created based on a document type, in this case we need to /// ensure the template markup is correct based on the model name of the document type /// private void FileService_SavingTemplate(IFileService sender, SaveEventArgs e) { // don't do anything if the factory is not enabled // because, no factory = no models (even if generation is enabled) if (!_config.EnableFactory) { return; } // don't do anything if this special key is not found if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) { return; } // ensure we have the content type alias if (!e.AdditionalData.ContainsKey("ContentTypeAlias")) { throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found"); } foreach (ITemplate template in e.SavedEntities) { // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key // is found, then it means a new template is being created based on the creation of a document type if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) { // ensure is safe and always pascal cased, per razor standard // + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application var alias = e.AdditionalData["ContentTypeAlias"].ToString(); var name = template.Name; // will be the name of the content type since we are creating var className = UmbracoServices.GetClrName(_shortStringHelper, name, alias); var modelNamespace = _config.ModelsNamespace; // we do not support configuring this at the moment, so just let Umbraco use its default value // var modelNamespaceAlias = ...; var markup = ViewHelper.GetDefaultFileContent( modelClassName: className, modelNamespace: modelNamespace/*, modelNamespaceAlias: modelNamespaceAlias*/); // set the template content to the new markup template.Content = markup; } } } private void ContentModelBinder_ModelBindingException(object sender, ContentModelBinder.ModelBindingArgs args) { ModelsBuilderAssemblyAttribute sourceAttr = args.SourceType.Assembly.GetCustomAttribute(); ModelsBuilderAssemblyAttribute modelAttr = args.ModelType.Assembly.GetCustomAttribute(); // if source or model is not a ModelsBuider type... if (sourceAttr == null || modelAttr == null) { // if neither are ModelsBuilder types, give up entirely if (sourceAttr == null && modelAttr == null) { return; } // else report, but better not restart (loops?) args.Message.Append(" The "); args.Message.Append(sourceAttr == null ? "view model" : "source"); args.Message.Append(" is a ModelsBuilder type, but the "); args.Message.Append(sourceAttr != null ? "view model" : "source"); args.Message.Append(" is not. The application is in an unstable state and should be restarted."); return; } // both are ModelsBuilder types var pureSource = sourceAttr.PureLive; var pureModel = modelAttr.PureLive; if (sourceAttr.PureLive || modelAttr.PureLive) { if (pureSource == false || pureModel == false) { // only one is pure - report, but better not restart (loops?) args.Message.Append(pureSource ? " The content model is PureLive, but the view model is not." : " The view model is PureLive, but the content model is not."); args.Message.Append(" The application is in an unstable state and should be restarted."); } else { // both are pure - report, and if different versions, restart // if same version... makes no sense... and better not restart (loops?) var sourceVersion = args.SourceType.Assembly.GetName().Version; var modelVersion = args.ModelType.Assembly.GetName().Version; args.Message.Append(" Both view and content models are PureLive, with "); args.Message.Append(sourceVersion == modelVersion ? "same version. The application is in an unstable state and should be restarted." : "different versions. The application is in an unstable state and is going to be restarted."); args.Restart = sourceVersion != modelVersion; } } } } }