1 namespace StackExchange.Profiling.UI 2 { 3 using System; 4 using System.Collections.Concurrent; 5 using System.Collections.Generic; 6 using System.Globalization; 7 using System.IO; 8 using System.Linq; 9 using System.Text; 10 using System.Web; 11 using System.Web.Routing; 12 13 using StackExchange.Profiling.Helpers; 14 15 /// <summary> 16 /// Understands how to route and respond to MiniProfiler UI URLS. 17 /// </summary> 18 public class MiniProfilerHandler : IRouteHandler, IHttpHandler 19 { 20 /// <summary> 21 /// Embedded resource contents keyed by filename. 22 /// </summary> 23 private static readonly ConcurrentDictionary<string, string> ResourceCache = new ConcurrentDictionary<string, string>(); 24 25 /// <summary> 26 /// The bypass local load. 27 /// </summary> 28 private static bool bypassLocalLoad = false; 29 30 /// <summary> 31 /// Gets a value indicating whether to keep things static and reusable. 32 /// </summary> 33 public bool IsReusable 34 { 35 get { return true; } 36 } 37 38 /// <summary> 39 /// Usually called internally, sometimes you may clear the routes during the apps lifecycle, if you do that call this to bring back mini profiler. 40 /// </summary> RegisterRoutes()41 public static void RegisterRoutes() 42 { 43 var routes = RouteTable.Routes; 44 var handler = new MiniProfilerHandler(); 45 var prefix = MiniProfiler.Settings.RouteBasePath.Replace("~/", string.Empty).EnsureTrailingSlash(); 46 47 using (routes.GetWriteLock()) 48 { 49 var route = new Route(prefix + "{filename}", handler) 50 { 51 // we have to specify these, so no MVC route helpers will match, e.g. @Html.ActionLink("Home", "Index", "Home") 52 Defaults = new RouteValueDictionary(new { controller = "MiniProfilerHandler", action = "ProcessRequest" }), 53 Constraints = new RouteValueDictionary(new { controller = "MiniProfilerHandler", action = "ProcessRequest" }) 54 }; 55 56 // put our routes at the beginning, like a boss 57 routes.Insert(0, route); 58 } 59 } 60 61 /// <summary> 62 /// Returns this <see cref="MiniProfilerHandler"/> to handle <paramref name="requestContext"/>. 63 /// </summary> 64 /// <param name="requestContext"> 65 /// The request Context. 66 /// </param> 67 /// <returns>the http handler implementation</returns> GetHttpHandler(RequestContext requestContext)68 public IHttpHandler GetHttpHandler(RequestContext requestContext) 69 { 70 return this; // elegant? I THINK SO. 71 } 72 73 /// <summary> 74 /// Returns either includes' <c>css/javascript</c> or results' html. 75 /// </summary> 76 /// <param name="context">The http context.</param> ProcessRequest(HttpContext context)77 public void ProcessRequest(HttpContext context) 78 { 79 string output; 80 string path = context.Request.AppRelativeCurrentExecutionFilePath; 81 82 switch (Path.GetFileNameWithoutExtension(path).ToLowerInvariant()) 83 { 84 case "jquery.1.7.1": 85 case "jquery.tmpl": 86 case "includes": 87 case "list": 88 output = Includes(context, path); 89 break; 90 91 case "results-index": 92 output = Index(context); 93 break; 94 95 case "results-list": 96 output = ResultList(context); 97 break; 98 99 case "results": 100 output = Results(context); 101 break; 102 103 default: 104 output = NotFound(context); 105 break; 106 } 107 108 context.Response.Write(output); 109 } 110 111 /// <summary> 112 /// Renders script tag found in "include.partial.html" - this is shared with all other language implementations, so if you change it, you MUST 113 /// provide changes for those other implementations, e.g. ruby. 114 /// </summary> RenderIncludes( MiniProfiler profiler, RenderPosition? position = null, bool? showTrivial = null, bool? showTimeWithChildren = null, int? maxTracesToShow = null, bool? showControls = null, bool? startHidden = null)115 internal static HtmlString RenderIncludes( 116 MiniProfiler profiler, 117 RenderPosition? position = null, 118 bool? showTrivial = null, 119 bool? showTimeWithChildren = null, 120 int? maxTracesToShow = null, 121 bool? showControls = null, 122 bool? startHidden = null) 123 { 124 if (profiler == null) return new HtmlString(""); 125 126 MiniProfiler.Settings.EnsureStorageStrategy(); 127 var authorized = MiniProfiler.Settings.Results_Authorize == null || MiniProfiler.Settings.Results_Authorize(HttpContext.Current.Request); 128 129 // unviewed ids are added to this list during Storage.Save, but we know we haven't see the current one yet, so go ahead and add it to the end 130 var ids = authorized ? MiniProfiler.Settings.Storage.GetUnviewedIds(profiler.User) : new List<Guid>(); 131 ids.Add(profiler.Id); 132 133 var format = GetResource("include.partial.html"); 134 var result = format.Format(new 135 { 136 path = VirtualPathUtility.ToAbsolute(MiniProfiler.Settings.RouteBasePath).EnsureTrailingSlash(), 137 version = MiniProfiler.Settings.Version, 138 ids = string.Join(",", ids.Select(guid => guid.ToString())), 139 position = (position ?? MiniProfiler.Settings.PopupRenderPosition).ToString().ToLower(), 140 showTrivial = (showTrivial ?? MiniProfiler.Settings.PopupShowTrivial).ToJs(), 141 showChildren = (showTimeWithChildren ?? MiniProfiler.Settings.PopupShowTimeWithChildren).ToJs(), 142 maxTracesToShow = maxTracesToShow ?? MiniProfiler.Settings.PopupMaxTracesToShow, 143 showControls = (showControls ?? MiniProfiler.Settings.ShowControls).ToJs(), 144 currentId = profiler.Id, 145 authorized = authorized.ToJs(), 146 toggleShortcut = MiniProfiler.Settings.PopupToggleKeyboardShortcut, 147 startHidden = (startHidden ?? MiniProfiler.Settings.PopupStartHidden).ToJs() 148 }); 149 150 return new HtmlString(result); 151 } 152 153 /// <summary> 154 /// The result list. 155 /// </summary> 156 /// <param name="context">The context.</param> 157 /// <returns>a string containing the result list.</returns> ResultList(HttpContext context)158 private static string ResultList(HttpContext context) 159 { 160 string message; 161 if (!AuthorizeRequest(context, isList: true, message: out message)) 162 { 163 return message; 164 } 165 166 var lastId = context.Request["last-id"]; 167 Guid lastGuid = Guid.Empty; 168 169 if (!lastId.IsNullOrWhiteSpace()) 170 { 171 Guid.TryParse(lastId, out lastGuid); 172 } 173 174 // After app restart, MiniProfiler.Settings.Storage will be null if no results saved, and NullReferenceException is thrown. 175 if (MiniProfiler.Settings.Storage == null) 176 { 177 MiniProfiler.Settings.EnsureStorageStrategy(); 178 } 179 180 var guids = MiniProfiler.Settings.Storage.List(100); 181 182 if (lastGuid != Guid.Empty) 183 { 184 guids = guids.TakeWhile(g => g != lastGuid); 185 } 186 187 guids = guids.Reverse(); 188 189 return guids.Select( 190 g => 191 { 192 var profiler = MiniProfiler.Settings.Storage.Load(g); 193 return 194 new 195 { 196 profiler.Id, 197 profiler.Name, 198 profiler.DurationMilliseconds, 199 profiler.DurationMillisecondsInSql, 200 profiler.ClientTimings, 201 profiler.Started, 202 profiler.ExecutedNonQueries, 203 profiler.ExecutedReaders, 204 profiler.ExecutedScalars, 205 profiler.HasAllTrivialTimings, 206 profiler.HasDuplicateSqlTimings, 207 profiler.HasSqlTimings, 208 profiler.HasTrivialTimings, 209 profiler.HasUserViewed, 210 profiler.MachineName, 211 profiler.User 212 }; 213 }).ToJson(); 214 } 215 216 /// <summary> 217 /// the index (Landing) view. 218 /// </summary> 219 /// <param name="context">The context.</param> 220 /// <returns>a string containing the html.</returns> Index(HttpContext context)221 private static string Index(HttpContext context) 222 { 223 string message; 224 if (!AuthorizeRequest(context, isList: true, message: out message)) 225 { 226 return message; 227 } 228 229 context.Response.ContentType = "text/html"; 230 231 var path = VirtualPathUtility.ToAbsolute(MiniProfiler.Settings.RouteBasePath).EnsureTrailingSlash(); 232 return new StringBuilder() 233 .AppendLine("<html><head>") 234 .AppendFormat("<title>List of profiling sessions</title>") 235 .AppendLine() 236 .AppendLine("<script type='text/javascript' src='" + path + "jquery.1.7.1.js?v=" + MiniProfiler.Settings.Version + "'></script>") 237 .AppendLine("<script id='mini-profiler' data-ids='' type='text/javascript' src='" + path + "includes.js?v=" + MiniProfiler.Settings.Version + "'></script>") 238 .AppendLine("<script type='text/javascript' src='" + path + "jquery.tmpl.js?v=" + MiniProfiler.Settings.Version + "'></script>") 239 .AppendLine( 240 "<script type='text/javascript' src='" + path + "list.js?v=" + MiniProfiler.Settings.Version 241 + "'></script>") 242 .AppendLine( 243 "<link href='" + path + "list.css?v=" + MiniProfiler.Settings.Version 244 + "' rel='stylesheet' type='text/css'>") 245 .AppendLine( 246 "<script type='text/javascript'>MiniProfiler.list.init({path: '" + path + "', version: '" 247 + MiniProfiler.Settings.Version + "'})</script>") 248 .AppendLine("</head><body></body></html>") 249 .ToString(); 250 } 251 252 /// <summary> 253 /// Handles rendering static content files. 254 /// </summary> 255 /// <param name="context">The context.</param> 256 /// <param name="path">The path.</param> 257 /// <returns>a string containing the content type.</returns> Includes(HttpContext context, string path)258 private static string Includes(HttpContext context, string path) 259 { 260 var response = context.Response; 261 switch (Path.GetExtension(path)) 262 { 263 case ".js": 264 response.ContentType = "application/javascript"; 265 break; 266 case ".css": 267 response.ContentType = "text/css"; 268 break; 269 case ".tmpl": 270 response.ContentType = "text/x-jquery-tmpl"; 271 break; 272 default: 273 return NotFound(context); 274 } 275 #if !DEBUG 276 var cache = response.Cache; 277 cache.SetCacheability(System.Web.HttpCacheability.Public); 278 cache.SetExpires(DateTime.Now.AddDays(7)); 279 cache.SetValidUntilExpires(true); 280 #endif 281 282 var embeddedFile = Path.GetFileName(path); 283 return GetResource(embeddedFile); 284 } 285 286 /// <summary> 287 /// Handles rendering a previous <c>MiniProfiler</c> session, identified by its <c>"?id=GUID"</c> on the query. 288 /// </summary> 289 /// <param name="context">The context.</param> 290 /// <returns>a string containing the rendered content</returns> Results(HttpContext context)291 private static string Results(HttpContext context) 292 { 293 // when we're rendering as a button/popup in the corner, we'll pass ?popup=1 294 // if it's absent, we're rendering results as a full page for sharing 295 var isPopup = !string.IsNullOrWhiteSpace(context.Request["popup"]); 296 297 // this guid is the MiniProfiler.Id property 298 // if this guid is not supplied, the last set of results needs to be 299 // displayed. The home page doesn't have profiling otherwise. 300 Guid id; 301 if (!Guid.TryParse(context.Request["id"], out id)) 302 id = MiniProfiler.Settings.Storage.List(1).FirstOrDefault(); 303 304 if (id == default(Guid)) 305 return isPopup ? NotFound(context) : NotFound(context, "text/plain", "No Guid id specified on the query string"); 306 307 MiniProfiler.Settings.EnsureStorageStrategy(); 308 var profiler = MiniProfiler.Settings.Storage.Load(id); 309 310 var provider = WebRequestProfilerProvider.Settings.UserProvider; 311 string user = null; 312 if (provider != null) 313 { 314 user = provider.GetUser(context.Request); 315 } 316 317 MiniProfiler.Settings.Storage.SetViewed(user, id); 318 319 if (profiler == null) 320 { 321 return isPopup ? NotFound(context) : NotFound(context, "text/plain", "No MiniProfiler results found with Id=" + id.ToString()); 322 } 323 324 bool needsSave = false; 325 if (profiler.ClientTimings == null) 326 { 327 profiler.ClientTimings = ClientTimings.FromRequest(context.Request); 328 if (profiler.ClientTimings != null) 329 { 330 needsSave = true; 331 } 332 } 333 334 if (profiler.HasUserViewed == false) 335 { 336 profiler.HasUserViewed = true; 337 needsSave = true; 338 } 339 340 if (needsSave) MiniProfiler.Settings.Storage.Save(profiler); 341 342 var authorize = MiniProfiler.Settings.Results_Authorize; 343 344 if (authorize != null && !authorize(context.Request)) 345 { 346 context.Response.ContentType = "application/json"; 347 return "hidden".ToJson(); 348 } 349 350 return isPopup ? ResultsJson(context, profiler) : ResultsFullPage(context, profiler); 351 } 352 353 /// <summary> 354 /// authorize the request. 355 /// </summary> 356 /// <param name="context">The context.</param> 357 /// <param name="isList">is list.</param> 358 /// <param name="message">The message.</param> 359 /// <returns>true if the request is authorised.</returns> AuthorizeRequest(HttpContext context, bool isList, out string message)360 private static bool AuthorizeRequest(HttpContext context, bool isList, out string message) 361 { 362 message = null; 363 var authorize = MiniProfiler.Settings.Results_Authorize; 364 var authorizeList = MiniProfiler.Settings.Results_List_Authorize; 365 366 if ((authorize != null && !authorize(context.Request)) || (isList && (authorizeList == null || !authorizeList(context.Request)))) 367 { 368 context.Response.StatusCode = 401; 369 context.Response.ContentType = "text/plain"; 370 message = "unauthorized"; 371 return false; 372 } 373 374 return true; 375 } 376 377 /// <summary> 378 /// set the JSON results and the content type. 379 /// </summary> 380 /// <param name="context">The context.</param> 381 /// <param name="profiler">The profiler.</param> 382 /// <returns>a string containing the JSON results.</returns> ResultsJson(HttpContext context, MiniProfiler profiler)383 private static string ResultsJson(HttpContext context, MiniProfiler profiler) 384 { 385 context.Response.ContentType = "application/json"; 386 return MiniProfiler.ToJson(profiler); 387 } 388 389 /// <summary> 390 /// results full page. 391 /// </summary> 392 /// <param name="context">The context.</param> 393 /// <param name="profiler">The profiler.</param> 394 /// <returns>a string containing the results page</returns> ResultsFullPage(HttpContext context, MiniProfiler profiler)395 private static string ResultsFullPage(HttpContext context, MiniProfiler profiler) 396 { 397 context.Response.ContentType = "text/html"; 398 399 var template = GetResource("share.html"); 400 return template.Format(new 401 { 402 name = profiler.Name, 403 duration = profiler.DurationMilliseconds.ToString(CultureInfo.InvariantCulture), 404 path = VirtualPathUtility.ToAbsolute(MiniProfiler.Settings.RouteBasePath).EnsureTrailingSlash(), 405 json = MiniProfiler.ToJson(profiler), 406 includes = RenderIncludes(profiler), 407 version = MiniProfiler.Settings.Version 408 }); 409 } 410 411 /// <summary> 412 /// get the resource. 413 /// </summary> 414 /// <param name="filename">The filename.</param> 415 /// <returns>a string containing the resource</returns> GetResource(string filename)416 private static string GetResource(string filename) 417 { 418 filename = filename.ToLower(); 419 string result; 420 421 #if DEBUG 422 // attempt to simply load from file system, this lets up modify js without needing to recompile A MILLION TIMES 423 if (!bypassLocalLoad) 424 { 425 426 var trace = new System.Diagnostics.StackTrace(true); 427 var path = System.IO.Path.GetDirectoryName(trace.GetFrames()[0].GetFileName()) + "\\..\\UI\\" + filename; 428 try 429 { 430 return File.ReadAllText(path); 431 } 432 catch 433 { 434 bypassLocalLoad = true; 435 } 436 } 437 #endif 438 439 if (!ResourceCache.TryGetValue(filename, out result)) 440 { 441 string customTemplatesPath = HttpContext.Current.Server.MapPath(MiniProfiler.Settings.CustomUITemplates); 442 string customTemplateFile = Path.Combine(customTemplatesPath, filename); 443 444 if (File.Exists(customTemplateFile)) 445 { 446 result = File.ReadAllText(customTemplateFile); 447 } 448 else 449 { 450 using (var stream = typeof(MiniProfilerHandler).Assembly.GetManifestResourceStream("StackExchange.Profiling.UI." + filename)) 451 using (var reader = new StreamReader(stream)) 452 { 453 result = reader.ReadToEnd(); 454 } 455 } 456 457 ResourceCache[filename] = result; 458 } 459 460 return result; 461 } 462 463 /// <summary> 464 /// Helper method that sets a proper 404 response code. 465 /// </summary> 466 /// <param name="context">The context.</param> 467 /// <param name="contentType">The content Type.</param> 468 /// <param name="message">The message.</param> 469 /// <returns>a string containing the 'not found' message.</returns> NotFound(HttpContext context, string contentType = R, string message = null)470 private static string NotFound(HttpContext context, string contentType = "text/plain", string message = null) 471 { 472 context.Response.StatusCode = 404; 473 context.Response.ContentType = contentType; 474 475 return message; 476 } 477 478 } 479 } 480