using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Common.ScheduledTasks.Tasks; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Tasks; using ServiceStack.ServiceHost; using ServiceStack.WebHost.Endpoints; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace MediaBrowser.WebDashboard.Api { /// /// Class GetDashboardConfigurationPages /// [Route("/dashboard/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")] public class GetDashboardConfigurationPage : IReturn { /// /// Gets or sets the name. /// /// The name. public string Name { get; set; } } /// /// Class GetDashboardResource /// public class GetDashboardResource { /// /// Gets or sets the name. /// /// The name. public string Name { get; set; } /// /// Gets or sets the V. /// /// The V. public string V { get; set; } } /// /// Class GetDashboardInfo /// [Route("/dashboard/dashboardInfo", "GET")] public class GetDashboardInfo : IReturn { } /// /// Class DashboardService /// [Export(typeof(IRestfulService))] public class DashboardService : BaseRestService { /// /// Adds the routes. /// /// The app host. public override void Configure(IAppHost appHost) { base.Configure(appHost); appHost.Routes.Add("/dashboard/{name*}", "GET"); } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetDashboardInfo request) { var kernel = (Kernel)Kernel; return GetDashboardInfo(kernel, Logger); } /// /// Gets the dashboard info. /// /// The kernel. /// The logger. /// DashboardInfo. public static DashboardInfo GetDashboardInfo(Kernel kernel, ILogger logger) { var connections = kernel.UserManager.ActiveConnections.ToArray(); var dtoBuilder = new DtoBuilder(logger); return new DashboardInfo { SystemInfo = kernel.GetSystemInfo(), RunningTasks = kernel.ScheduledTasks.Where(i => i.State == TaskState.Running || i.State == TaskState.Cancelling) .Select(ScheduledTaskHelpers.GetTaskInfo) .ToArray(), ApplicationUpdateTaskId = kernel.ScheduledTasks.OfType().First().Id, ActiveConnections = connections, Users = kernel.Users.Where(u => connections.Any(c => c.UserId == u.Id)).Select(dtoBuilder.GetDtoUser).ToArray() }; } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetDashboardConfigurationPage request) { var kernel = (Kernel)Kernel; var page = kernel.PluginConfigurationPages.First(p => p.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase)); return ToStaticResult(page.Version.GetMD5(), page.DateLastModified, null, MimeTypes.GetMimeType("page.html"), () => ModifyHtml(page.GetHtmlStream())); } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetDashboardConfigurationPages request) { var kernel = (Kernel)Kernel; var pages = kernel.PluginConfigurationPages; if (request.PageType.HasValue) { pages = pages.Where(p => p.ConfigurationPageType == request.PageType.Value); } return ToOptimizedResult(pages.ToList()); } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetDashboardResource request) { var path = request.Name; var contentType = MimeTypes.GetMimeType(path); 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) || !string.IsNullOrEmpty(request.V)) { cacheDuration = TimeSpan.FromDays(365); } var assembly = GetType().Assembly.GetName(); return ToStaticResult(assembly.Version.ToString().GetMD5(), null, cacheDuration, contentType, () => GetResourceStream(path)); } /// /// Gets the resource stream. /// /// The path. /// Task{Stream}. private async Task GetResourceStream(string path) { Stream resourceStream; if (path.Equals("scripts/all.js", StringComparison.OrdinalIgnoreCase)) { resourceStream = await GetAllJavascript().ConfigureAwait(false); } else { resourceStream = GetType().Assembly.GetManifestResourceStream("MediaBrowser.WebDashboard.Html." + ConvertUrlToResourcePath(path)); } if (resourceStream != null) { var isHtml = IsHtml(path); // Don't apply any caching for html pages // jQuery ajax doesn't seem to handle if-modified-since correctly if (isHtml) { resourceStream = await ModifyHtml(resourceStream).ConfigureAwait(false); } } return resourceStream; } /// /// Redirects the specified CTX. /// /// The CTX. /// The URL. private void Redirect(HttpListenerContext ctx, string url) { // Try to prevent the browser from caching the redirect response (the right way) ctx.Response.Headers[HttpResponseHeader.CacheControl] = "no-cache, no-store, must-revalidate"; ctx.Response.Headers[HttpResponseHeader.Pragma] = "no-cache, no-store, must-revalidate"; ctx.Response.Headers[HttpResponseHeader.Expires] = "-1"; ctx.Response.Redirect(url); ctx.Response.Close(); } /// /// Preserves the current query string when redirecting /// /// The request. /// The new URL. /// System.String. private string GetRedirectUrl(HttpListenerRequest request, string newUrl) { var query = request.Url.Query; return string.IsNullOrEmpty(query) ? newUrl : newUrl + query; } /// /// Converts the URL to a manifest resource path. /// /// The URL. /// System.String. private string ConvertUrlToResourcePath(string url) { var parts = url.Split('/'); var normalizedParts = new string[parts.Length]; for (var i = 0; i < parts.Length; i++) { // We have to do some tricky string replacements for all parts of the path except the last if (i < parts.Length - 1) { // Find the index of the first period as well as the first dash var periodIndex = parts[i].IndexOf('.'); var slashIndex = parts[i].IndexOf('-'); // Replace all periods with "._" and dashes with "_" normalizedParts[i] = parts[i].Replace(".", "._").Replace("-", "_"); // If the first period occurred before the first slash, change it back from "._" to just "." if (periodIndex < slashIndex) { var regex = new Regex("\\._"); normalizedParts[i] = regex.Replace(normalizedParts[i], ".", 1); } } else { normalizedParts[i] = parts[i]; } } return string.Join(".", normalizedParts); } /// /// 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); } /// /// Modifies the HTML by adding common meta tags, css and js. /// /// The source stream. /// Task{Stream}. internal async Task ModifyHtml(Stream sourceStream) { string html; using (var memoryStream = new MemoryStream()) { await sourceStream.CopyToAsync(memoryStream).ConfigureAwait(false); html = Encoding.UTF8.GetString(memoryStream.ToArray()); } var version = GetType().Assembly.GetName().Version; html = html.Replace("", "" + GetMetaTags() + GetCommonCss(version) + GetCommonJavascript(version)); var bytes = Encoding.UTF8.GetBytes(html); sourceStream.Dispose(); return new MemoryStream(bytes); } /// /// Gets the meta tags. /// /// System.String. private static string GetMetaTags() { var sb = new StringBuilder(); sb.Append(""); sb.Append(""); sb.Append(""); // http://developer.apple.com/library/ios/#DOCUMENTATION/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); return sb.ToString(); } /// /// Gets the common CSS. /// /// System.String. private static string GetCommonCss(Version version) { var versionString = "?v=" + version; var files = new[] { "http://code.jquery.com/mobile/1.3.0-rc.1/jquery.mobile-1.3.0-rc.1.min.css", "thirdparty/jqm-icon-pack-3.0/font-awesome/jqm-icon-pack-3.0.0-fa.css", "css/site.css" + versionString }; var tags = files.Select(s => string.Format("", s)).ToArray(); return string.Join(string.Empty, tags); } /// /// Gets the common javascript. /// /// System.String. private static string GetCommonJavascript(Version version) { var versionString = "?v=" + version; var files = new[] { "http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js", "http://code.jquery.com/mobile/1.3.0-rc.1/jquery.mobile-1.3.0-rc.1.min.js", "../jsapiclient.js" + versionString, "scripts/all.js" + versionString }; var tags = files.Select(s => string.Format("", s)).ToArray(); return string.Join(string.Empty, tags); } /// /// Gets a stream containing all concatenated javascript /// /// Task{Stream}. private async Task GetAllJavascript() { const string resourcePrefix = "MediaBrowser.WebDashboard.Html.scripts."; var assembly = GetType().Assembly; var scriptFiles = new[] { "Extensions.js", "Site.js", "AddPluginPage.js", "AdvancedConfigurationPage.js", "AdvancedMetadataConfigurationPage.js", "PluginCatalogPage.js", "DashboardPage.js", "DisplaySettingsPage.js", "EditUserPage.js", "IndexPage.js", "ItemDetailPage.js", "LoginPage.js", "LogPage.js", "MediaLibraryPage.js", "MediaPlayer.js", "MetadataConfigurationPage.js", "MetadataImagesPage.js", "PluginsPage.js", "PluginUpdatesPage.js", "ScheduledTaskPage.js", "ScheduledTasksPage.js", "UpdatePasswordPage.js", "UserImagePage.js", "UserProfilesPage.js", "WizardStartPage.js", "WizardUserPage.js", "SupporterKeyPage.js", "SupporterPage.js" }; var memoryStream = new MemoryStream(); var newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine); foreach (var file in scriptFiles) { using (var stream = assembly.GetManifestResourceStream(resourcePrefix + file)) { await stream.CopyToAsync(memoryStream).ConfigureAwait(false); await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false); } } memoryStream.Position = 0; return memoryStream; } } }