using System.Text; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using ServiceStack; using ServiceStack.Web; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using WebMarkupMin.Core.Minifiers; namespace MediaBrowser.WebDashboard.Api { /// /// Class GetDashboardConfigurationPages /// [Route("/dashboard/ConfigurationPages", "GET")] [Route("/web/ConfigurationPages", "GET")] public class GetDashboardConfigurationPages : IReturn> { /// /// Gets or sets the type of the page. /// /// The type of the page. public ConfigurationPageType? PageType { get; set; } } /// /// Class GetDashboardConfigurationPage /// [Route("/dashboard/ConfigurationPage", "GET")] [Route("/web/ConfigurationPage", "GET")] public class GetDashboardConfigurationPage { /// /// Gets or sets the name. /// /// The name. public string Name { get; set; } } [Route("/web/Package", "GET")] [Route("/dashboard/Package", "GET")] public class GetDashboardPackage { public string Mode { get; set; } } /// /// Class GetDashboardResource /// [Route("/web/{ResourceName*}", "GET")] [Route("/dashboard/{ResourceName*}", "GET")] public class GetDashboardResource { /// /// Gets or sets the name. /// /// The name. public string ResourceName { get; set; } /// /// Gets or sets the V. /// /// The V. public string V { get; set; } } /// /// Class DashboardService /// public class DashboardService : IRestfulService, IHasResultFactory { /// /// Gets or sets the logger. /// /// The logger. public ILogger Logger { get; set; } /// /// Gets or sets the HTTP result factory. /// /// The HTTP result factory. public IHttpResultFactory ResultFactory { get; set; } /// /// Gets or sets the request context. /// /// The request context. public IRequest Request { get; set; } /// /// The _app host /// private readonly IServerApplicationHost _appHost; /// /// The _server configuration manager /// private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; private readonly IJsonSerializer _jsonSerializer; /// /// Initializes a new instance of the class. /// /// The app host. /// The server configuration manager. /// The file system. public DashboardService(IServerApplicationHost appHost, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer) { _appHost = appHost; _serverConfigurationManager = serverConfigurationManager; _fileSystem = fileSystem; _localization = localization; _jsonSerializer = jsonSerializer; } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetDashboardConfigurationPage request) { var page = ServerEntryPoint.Instance.PluginConfigurationPages.First(p => p.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase)); return ResultFactory.GetStaticResult(Request, page.Plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator().ModifyHtml(page.GetHtmlStream(), null, null, false)); } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetDashboardConfigurationPages request) { const string unavilableMessage = "The server is still loading. Please try again momentarily."; var instance = ServerEntryPoint.Instance; if (instance == null) { throw new InvalidOperationException(unavilableMessage); } var pages = instance.PluginConfigurationPages; if (pages == null) { throw new InvalidOperationException(unavilableMessage); } if (request.PageType.HasValue) { pages = pages.Where(p => p.ConfigurationPageType == request.PageType.Value); } // Don't allow a failing plugin to fail them all var configPages = pages.Select(p => { try { return new ConfigurationPageInfo(p); } catch (Exception ex) { Logger.ErrorException("Error getting plugin information from {0}", ex, p.GetType().Name); return null; } }) .Where(i => i != null) .ToList(); return ResultFactory.GetOptimizedResult(Request, configPages); } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetDashboardResource request) { var path = request.ResourceName; var contentType = MimeTypes.GetMimeType(path); var isHtml = IsHtml(path); if (isHtml && !_serverConfigurationManager.Configuration.IsStartupWizardCompleted) { if (path.IndexOf("wizard", StringComparison.OrdinalIgnoreCase) == -1) { Request.Response.Redirect("wizardstart.html"); return null; } } path = path.Replace("scripts/jquery.mobile-1.4.5.min.map", "thirdparty/jquerymobile-1.4.5/jquery.mobile-1.4.5.min.map", StringComparison.OrdinalIgnoreCase); var localizationCulture = GetLocalizationCulture(); // Don't cache if not configured to do so // But always cache images to simulate production if (!_serverConfigurationManager.Configuration.EnableDashboardResponseCaching && !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) && !contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase)) { return ResultFactory.GetResult(GetResourceStream(path, localizationCulture).Result, contentType); } TimeSpan? cacheDuration = null; // Cache images unconditionally - updates to image files will require new filename // If there's a version number in the query string we can cache this unconditionally if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrEmpty(request.V)) { cacheDuration = TimeSpan.FromDays(365); } var assembly = GetType().Assembly.GetName(); var cacheKey = (assembly.Version + (localizationCulture ?? string.Empty) + path).GetMD5(); return ResultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(path, localizationCulture)); } private string GetLocalizationCulture() { return _serverConfigurationManager.Configuration.UICulture; } /// /// Gets the resource stream. /// /// The path. /// The localization culture. /// Task{Stream}. private Task GetResourceStream(string path, string localizationCulture) { var minify = _serverConfigurationManager.Configuration.EnableDashboardResourceMinification; return GetPackageCreator() .GetResource(path, null, localizationCulture, _appHost.ApplicationVersion.ToString(), minify); } private PackageCreator GetPackageCreator() { return new PackageCreator(_fileSystem, _localization, Logger, _serverConfigurationManager, _jsonSerializer); } /// /// Determines whether the specified path is HTML. /// /// The path. /// true if the specified path is HTML; otherwise, false. private bool IsHtml(string path) { return Path.GetExtension(path).EndsWith("html", StringComparison.OrdinalIgnoreCase); } private void CopyFile(string src, string dst) { Directory.CreateDirectory(Path.GetDirectoryName(dst)); File.Copy(src, dst, true); } public async Task Get(GetDashboardPackage request) { var path = Path.Combine(_serverConfigurationManager.ApplicationPaths.ProgramDataPath, "webclient-dump"); try { _fileSystem.DeleteDirectory(path, true); } catch (IOException) { } var creator = GetPackageCreator(); CopyDirectory(creator.DashboardUIPath, path); var culture = "en-US"; var appVersion = _appHost.ApplicationVersion.ToString(); var mode = request.Mode; if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase)) { // Overwrite certain files with cordova specific versions var cordovaVersion = Path.Combine(path, "cordova", "registrationservices.js"); File.Copy(cordovaVersion, Path.Combine(path, "scripts", "registrationservices.js"), true); File.Delete(cordovaVersion); // Delete things that are unneeded in an attempt to keep the output as trim as possible Directory.Delete(Path.Combine(path, "css", "images", "tour"), true); Directory.Delete(Path.Combine(path, "apiclient", "alt"), true); File.Delete(Path.Combine(path, "thirdparty", "jquerymobile-1.4.5", "jquery.mobile-1.4.5.min.map")); Directory.Delete(Path.Combine(path, "bower_components"), true); Directory.Delete(Path.Combine(path, "thirdparty", "viblast"), true); // But we do need this CopyFile(Path.Combine(creator.DashboardUIPath, "bower_components", "webcomponentsjs", "webcomponents-lite.min.js"), Path.Combine(path, "bower_components", "webcomponentsjs", "webcomponents-lite.min.js")); CopyFile(Path.Combine(creator.DashboardUIPath, "bower_components", "velocity", "velocity.min.js"), Path.Combine(path, "bower_components", "velocity", "velocity.min.js")); CopyDirectory(Path.Combine(creator.DashboardUIPath, "bower_components", "swipebox", "src", "css"), Path.Combine(path, "bower_components", "swipebox", "src", "css")); CopyDirectory(Path.Combine(creator.DashboardUIPath, "bower_components", "swipebox", "src", "js"), Path.Combine(path, "bower_components", "swipebox", "src", "js")); CopyDirectory(Path.Combine(creator.DashboardUIPath, "bower_components", "swipebox", "src", "img"), Path.Combine(path, "bower_components", "swipebox", "src", "img")); } MinifyCssDirectory(Path.Combine(path, "css")); MinifyJsDirectory(Path.Combine(path, "scripts")); MinifyJsDirectory(Path.Combine(path, "apiclient")); MinifyJsDirectory(Path.Combine(path, "voice")); await DumpHtml(creator.DashboardUIPath, path, mode, culture, appVersion); await DumpJs(creator.DashboardUIPath, path, mode, culture, appVersion); await DumpFile("scripts/all.js", Path.Combine(path, "scripts", "all.js"), mode, culture, appVersion).ConfigureAwait(false); await DumpFile("css/all.css", Path.Combine(path, "css", "all.css"), mode, culture, appVersion).ConfigureAwait(false); return ""; } private void MinifyCssDirectory(string path) { foreach (var file in Directory.GetFiles(path, "*.css", SearchOption.AllDirectories)) { try { var text = File.ReadAllText(file, Encoding.UTF8); var result = new KristensenCssMinifier().Minify(text, false, Encoding.UTF8); if (result.Errors.Count > 0) { Logger.Error("Error minifying css: " + result.Errors[0].Message); } else { text = result.MinifiedContent; File.WriteAllText(file, text, Encoding.UTF8); } } catch (Exception ex) { Logger.ErrorException("Error minifying css", ex); } } } private void MinifyJsDirectory(string path) { foreach (var file in Directory.GetFiles(path, "*.js", SearchOption.AllDirectories)) { try { var text = File.ReadAllText(file, Encoding.UTF8); var result = new CrockfordJsMinifier().Minify(text, false, Encoding.UTF8); if (result.Errors.Count > 0) { Logger.Error("Error minifying javascript: " + result.Errors[0].Message); } else { text = result.MinifiedContent; File.WriteAllText(file, text, Encoding.UTF8); } } catch (Exception ex) { Logger.ErrorException("Error minifying css", ex); } } } private async Task DumpHtml(string source, string destination, string mode, string culture, string appVersion) { foreach (var file in Directory.GetFiles(source, "*.html", SearchOption.TopDirectoryOnly)) { var filename = Path.GetFileName(file); await DumpFile(filename, Path.Combine(destination, filename), mode, culture, appVersion).ConfigureAwait(false); } var excludeFiles = new List(); if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase)) { excludeFiles.Add("supporter.html"); excludeFiles.Add("supporterkey.html"); } foreach (var file in excludeFiles) { File.Delete(Path.Combine(destination, file)); } } private async Task DumpJs(string source, string mode, string destination, string culture, string appVersion) { foreach (var file in Directory.GetFiles(source, "*.js", SearchOption.TopDirectoryOnly)) { var filename = Path.GetFileName(file); await DumpFile("scripts/" + filename, Path.Combine(destination, "scripts", filename), mode, culture, appVersion).ConfigureAwait(false); } } private async Task DumpFile(string resourceVirtualPath, string destinationFilePath, string mode, string culture, string appVersion) { using (var stream = await GetPackageCreator().GetResource(resourceVirtualPath, mode, culture, appVersion, true).ConfigureAwait(false)) { using (var fs = _fileSystem.GetFileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.Read)) { stream.CopyTo(fs); } } } private void CopyDirectory(string source, string destination) { Directory.CreateDirectory(destination); //Now Create all of the directories foreach (string dirPath in Directory.GetDirectories(source, "*", SearchOption.AllDirectories)) Directory.CreateDirectory(dirPath.Replace(source, destination)); //Copy all the files & Replaces any files with the same name foreach (string newPath in Directory.GetFiles(source, "*.*", SearchOption.AllDirectories)) File.Copy(newPath, newPath.Replace(source, destination), true); } } }