1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
2 
3 using System.Collections.Generic;
4 using System.Diagnostics.CodeAnalysis;
5 using System.Globalization;
6 using System.IO;
7 using System.Text;
8 using System.Web.WebPages.Resources;
9 using Microsoft.Internal.Web.Utils;
10 
11 namespace System.Web.WebPages
12 {
13     // TODO(elipton): Clean this up and remove the suppression
14     [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "This is temporary (elipton)")]
15     public abstract class WebPageBase : WebPageRenderingBase
16     {
17         // Keep track of which sections RenderSection has already been called on
18         private HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
19         // Keep track of whether RenderBody has been called
20         private bool _renderedBody = false;
21         // Action for rendering the body within a layout page
22         private Action<TextWriter> _body;
23 
24         // TODO(elipton): Figure out if we still need these two writers
25         private TextWriter _tempWriter;
26         private TextWriter _currentWriter;
27 
28         private DynamicPageDataDictionary<dynamic> _dynamicPageData;
29 
30         public override string Layout { get; set; }
31 
32         public TextWriter Output
33         {
34             get { return OutputStack.Peek(); }
35         }
36 
37         public Stack<TextWriter> OutputStack
38         {
39             get { return PageContext.OutputStack; }
40         }
41 
42         public override IDictionary<object, dynamic> PageData
43         {
44             get { return PageContext.PageData; }
45         }
46 
47         public override dynamic Page
48         {
49             get
50             {
51                 if (_dynamicPageData == null)
52                 {
53                     _dynamicPageData = new DynamicPageDataDictionary<dynamic>((PageDataDictionary<dynamic>)PageData);
54                 }
55                 return _dynamicPageData;
56             }
57         }
58 
59         // Retrieves the sections defined in the calling page. If this is null, that means
60         // this page has been requested directly.
61         private Dictionary<string, SectionWriter> PreviousSectionWriters
62         {
63             get
64             {
65                 var top = SectionWritersStack.Pop();
66                 var previous = SectionWritersStack.Count > 0 ? SectionWritersStack.Peek() : null;
67                 SectionWritersStack.Push(top);
68                 return previous;
69             }
70         }
71 
72         // Retrieves the current Dictionary of sectionWriters on the stack without poping it.
73         // There should be at least one on the stack which is added when the Render(ViewData,TextWriter)
74         // is called.
75         private Dictionary<string, SectionWriter> SectionWriters
76         {
77             get { return SectionWritersStack.Peek(); }
78         }
79 
80         private Stack<Dictionary<string, SectionWriter>> SectionWritersStack
81         {
82             get { return PageContext.SectionWritersStack; }
83         }
84 
ConfigurePage(WebPageBase parentPage)85         protected virtual void ConfigurePage(WebPageBase parentPage)
86         {
87         }
88 
CreateInstanceFromVirtualPath(string virtualPath)89         public static WebPageBase CreateInstanceFromVirtualPath(string virtualPath)
90         {
91             return CreateInstanceFromVirtualPath(virtualPath, VirtualPathFactoryManager.Instance);
92         }
93 
CreateInstanceFromVirtualPath(string virtualPath, IVirtualPathFactory virtualPathFactory)94         internal static WebPageBase CreateInstanceFromVirtualPath(string virtualPath, IVirtualPathFactory virtualPathFactory)
95         {
96             // Get the compiled object
97             try
98             {
99                 WebPageBase webPage = virtualPathFactory.CreateInstance<WebPageBase>(virtualPath);
100 
101                 // Give it its virtual path
102                 webPage.VirtualPath = virtualPath;
103 
104                 // Assign it the VirtualPathFactory
105                 webPage.VirtualPathFactory = virtualPathFactory;
106 
107                 return webPage;
108             }
109             catch (HttpException e)
110             {
111                 BuildManagerExceptionUtil.ThrowIfUnsupportedExtension(virtualPath, e);
112                 throw;
113             }
114         }
115 
116         /// <summary>
117         /// Attempts to create a WebPageBase instance from a virtualPath and wraps complex compiler exceptions with simpler messages
118         /// </summary>
CreatePageFromVirtualPath(string virtualPath, HttpContextBase httpContext, Func<string, bool> virtualPathExists, DisplayModeProvider displayModeProvider, IDisplayMode displayMode)119         private WebPageBase CreatePageFromVirtualPath(string virtualPath, HttpContextBase httpContext, Func<string, bool> virtualPathExists, DisplayModeProvider displayModeProvider, IDisplayMode displayMode)
120         {
121             try
122             {
123                 DisplayInfo resolvedDisplayInfo = displayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, httpContext, virtualPathExists, displayMode);
124 
125                 if (resolvedDisplayInfo != null)
126                 {
127                     var webPage = VirtualPathFactory.CreateInstance<WebPageBase>(resolvedDisplayInfo.FilePath);
128 
129                     if (webPage != null)
130                     {
131                         // Give it its virtual path
132                         webPage.VirtualPath = virtualPath;
133                         webPage.VirtualPathFactory = VirtualPathFactory;
134                         webPage.DisplayModeProvider = DisplayModeProvider;
135 
136                         return webPage;
137                     }
138                 }
139             }
140             catch (HttpException e)
141             {
142                 // If the path uses an unregistered extension, such as Foo.txt,
143                 // then an error regarding build providers will be thrown.
144                 // Check if this is the case and throw a simpler error.
145                 BuildManagerExceptionUtil.ThrowIfUnsupportedExtension(virtualPath, e);
146 
147                 // If the path uses an extension registered with codedom, such as Foo.js,
148                 // then an unfriendly compilation error might get thrown by the underlying compiler.
149                 // Check if this is the case and throw a simpler error.
150                 BuildManagerExceptionUtil.ThrowIfCodeDomDefinedExtension(virtualPath, e);
151 
152                 // Rethrow any errors
153                 throw;
154             }
155             // The page is missing, could not be compiled or is of an invalid type.
156             throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_InvalidPageType, virtualPath));
157         }
158 
CreatePageContextFromParameters(bool isLayoutPage, params object[] data)159         private WebPageContext CreatePageContextFromParameters(bool isLayoutPage, params object[] data)
160         {
161             object first = null;
162             if (data != null && data.Length > 0)
163             {
164                 first = data[0];
165             }
166 
167             var pageData = PageDataDictionary<dynamic>.CreatePageDataFromParameters(PageData, data);
168 
169             return WebPageContext.CreateNestedPageContext(PageContext, pageData, first, isLayoutPage);
170         }
171 
DefineSection(string name, SectionWriter action)172         public void DefineSection(string name, SectionWriter action)
173         {
174             if (SectionWriters.ContainsKey(name))
175             {
176                 throw new HttpException(String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionAleadyDefined, name));
177             }
178             SectionWriters[name] = action;
179         }
180 
EnsurePageCanBeRequestedDirectly(string methodName)181         internal void EnsurePageCanBeRequestedDirectly(string methodName)
182         {
183             if (PreviousSectionWriters == null)
184             {
185                 throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_CannotRequestDirectly, VirtualPath, methodName));
186             }
187         }
188 
ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer)189         public void ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer)
190         {
191             ExecutePageHierarchy(pageContext, writer, startPage: null);
192         }
193 
194         // This method is only used by WebPageBase to allow passing in the view context and writer.
ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage)195         public void ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage)
196         {
197             PushContext(pageContext, writer);
198 
199             if (startPage != null)
200             {
201                 if (startPage != this)
202                 {
203                     var startPageContext = WebPageContext.CreateNestedPageContext<object>(parentContext: pageContext, pageData: null, model: null, isLayoutPage: false);
204                     startPageContext.Page = startPage;
205                     startPage.PageContext = startPageContext;
206                 }
207                 startPage.ExecutePageHierarchy();
208             }
209             else
210             {
211                 ExecutePageHierarchy();
212             }
213             PopContext();
214         }
215 
216         [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We really don't care if SourceHeader fails, and we don't want it to fail any real requests ever")]
ExecutePageHierarchy()217         public override void ExecutePageHierarchy()
218         {
219             // Unlike InitPages, for a WebPage there is no hierarchy - it is always
220             // the last file to execute in the chain. There can still be layout pages
221             // and partial pages, but they are never part of the hierarchy.
222 
223             // (add server header for falcon debugging)
224             // call to MapPath() is expensive. If we are not emiting source files to header,
225             // don't bother to populate the SourceFiles collection. This saves perf significantly.
226             if (WebPageHttpHandler.ShouldGenerateSourceHeader(Context))
227             {
228                 try
229                 {
230                     string vp = VirtualPath;
231                     if (vp != null)
232                     {
233                         string path = Context.Request.MapPath(vp);
234                         if (!path.IsEmpty())
235                         {
236                             PageContext.SourceFiles.Add(path);
237                         }
238                     }
239                 }
240                 catch
241                 {
242                     // we really don't care if this ever fails, so we swallow all exceptions
243                 }
244             }
245 
246             TemplateStack.Push(Context, this);
247             try
248             {
249                 // Execute the developer-written code of the WebPage
250                 Execute();
251             }
252             finally
253             {
254                 TemplateStack.Pop(Context);
255             }
256         }
257 
InitializePage()258         protected virtual void InitializePage()
259         {
260         }
261 
IsSectionDefined(string name)262         public bool IsSectionDefined(string name)
263         {
264             EnsurePageCanBeRequestedDirectly("IsSectionDefined");
265             return PreviousSectionWriters.ContainsKey(name);
266         }
267 
PopContext()268         public void PopContext()
269         {
270             string renderedContent = _tempWriter.ToString();
271             OutputStack.Pop();
272 
273             if (!String.IsNullOrEmpty(Layout))
274             {
275                 string layoutPagePath = NormalizeLayoutPagePath(Layout);
276                 // If a layout file was specified, render it passing our page content
277                 OutputStack.Push(_currentWriter);
278                 RenderSurrounding(
279                     layoutPagePath,
280                     w => w.Write(renderedContent));
281                 OutputStack.Pop();
282             }
283             else
284             {
285                 // Otherwise, just render the page
286                 _currentWriter.Write(renderedContent);
287             }
288 
289             VerifyRenderedBodyOrSections();
290             SectionWritersStack.Pop();
291         }
292 
PushContext(WebPageContext pageContext, TextWriter writer)293         public void PushContext(WebPageContext pageContext, TextWriter writer)
294         {
295             _currentWriter = writer;
296             PageContext = pageContext;
297             pageContext.Page = this;
298 
299             InitializePage();
300 
301             // Create a temporary writer
302             _tempWriter = new StringWriter(CultureInfo.InvariantCulture);
303 
304             // Render the page into it
305             OutputStack.Push(_tempWriter);
306             SectionWritersStack.Push(new Dictionary<string, SectionWriter>(StringComparer.OrdinalIgnoreCase));
307 
308             // If the body is defined in the ViewData, remove it and store it on the instance
309             // so that it won't affect rendering of partial pages when they call VerifyRenderedBodyOrSections
310             if (PageContext.BodyAction != null)
311             {
312                 _body = PageContext.BodyAction;
313                 PageContext.BodyAction = null;
314             }
315         }
316 
RenderBody()317         public HelperResult RenderBody()
318         {
319             EnsurePageCanBeRequestedDirectly("RenderBody");
320 
321             if (_renderedBody)
322             {
323                 throw new HttpException(WebPageResources.WebPage_RenderBodyAlreadyCalled);
324             }
325             _renderedBody = true;
326 
327             // _body should have previously been set in Render(ViewContext,TextWriter) if it
328             // was available in the ViewData.
329             if (_body != null)
330             {
331                 return new HelperResult(tw => _body(tw));
332             }
333             else
334             {
335                 throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_CannotRequestDirectly, VirtualPath, "RenderBody"));
336             }
337         }
338 
RenderPage(string path, params object[] data)339         public override HelperResult RenderPage(string path, params object[] data)
340         {
341             return RenderPageCore(path, isLayoutPage: false, data: data);
342         }
343 
RenderPageCore(string path, bool isLayoutPage, object[] data)344         private HelperResult RenderPageCore(string path, bool isLayoutPage, object[] data)
345         {
346             if (String.IsNullOrEmpty(path))
347             {
348                 throw ExceptionHelper.CreateArgumentNullOrEmptyException("path");
349             }
350 
351             return new HelperResult(writer =>
352             {
353                 path = NormalizePath(path);
354                 WebPageBase subPage = CreatePageFromVirtualPath(path, Context, VirtualPathFactory.Exists, DisplayModeProvider, DisplayMode);
355                 var pageContext = CreatePageContextFromParameters(isLayoutPage, data);
356 
357                 subPage.ConfigurePage(this);
358                 subPage.ExecutePageHierarchy(pageContext, writer);
359             });
360         }
361 
RenderSection(string name)362         public HelperResult RenderSection(string name)
363         {
364             return RenderSection(name, required: true);
365         }
366 
RenderSection(string name, bool required)367         public HelperResult RenderSection(string name, bool required)
368         {
369             EnsurePageCanBeRequestedDirectly("RenderSection");
370 
371             if (PreviousSectionWriters.ContainsKey(name))
372             {
373                 var result = new HelperResult(tw =>
374                 {
375                     if (_renderedSections.Contains(name))
376                     {
377                         throw new HttpException(String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionAleadyRendered, name));
378                     }
379                     var body = PreviousSectionWriters[name];
380                     // Since the body can also call RenderSection, we need to temporarily remove
381                     // the current sections from the stack.
382                     var top = SectionWritersStack.Pop();
383 
384                     bool pushed = false;
385                     try
386                     {
387                         if (Output != tw)
388                         {
389                             OutputStack.Push(tw);
390                             pushed = true;
391                         }
392 
393                         body();
394                     }
395                     finally
396                     {
397                         if (pushed)
398                         {
399                             OutputStack.Pop();
400                         }
401                     }
402                     SectionWritersStack.Push(top);
403                     _renderedSections.Add(name);
404                 });
405                 return result;
406             }
407             else if (required)
408             {
409                 // If the section is not found, and it is not optional, throw an error.
410                 throw new HttpException(String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionNotDefined, name));
411             }
412             else
413             {
414                 // If the section is optional and not found, then don't do anything.
415                 return null;
416             }
417         }
418 
RenderSurrounding(string partialViewName, Action<TextWriter> body)419         private void RenderSurrounding(string partialViewName, Action<TextWriter> body)
420         {
421             // Save the previous body action and set ours instead.
422             // This value will be retrieved by the sub-page being rendered when it runs
423             // Render(ViewData, TextWriter).
424             var priorValue = PageContext.BodyAction;
425             PageContext.BodyAction = body;
426 
427             // Render the layout file
428             Write(RenderPageCore(partialViewName, isLayoutPage: true, data: new object[0]));
429 
430             // Restore the state
431             PageContext.BodyAction = priorValue;
432         }
433 
434         // Verifies that RenderBody is called, or that RenderSection is called for all sections
VerifyRenderedBodyOrSections()435         private void VerifyRenderedBodyOrSections()
436         {
437             // The _body will be set within a layout page because PageContext.BodyAction was set by RenderSurrounding,
438             // which is only called in the case of rendering layout pages.
439             // Using RenderPage will not result in a _body being set in a partial page, thus the following checks for
440             // sections should not apply when RenderPage is called.
441             // Dev10 bug 928341
442             if (_body != null)
443             {
444                 if (SectionWritersStack.Count > 1 && PreviousSectionWriters != null && PreviousSectionWriters.Count > 0)
445                 {
446                     // There are sections defined. Check that all sections have been rendered.
447                     StringBuilder sectionsNotRendered = new StringBuilder();
448                     foreach (var name in PreviousSectionWriters.Keys)
449                     {
450                         if (!_renderedSections.Contains(name))
451                         {
452                             if (sectionsNotRendered.Length > 0)
453                             {
454                                 sectionsNotRendered.Append("; ");
455                             }
456                             sectionsNotRendered.Append(name);
457                         }
458                     }
459                     if (sectionsNotRendered.Length > 0)
460                     {
461                         throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_SectionsNotRendered, VirtualPath, sectionsNotRendered.ToString()));
462                     }
463                 }
464                 else if (!_renderedBody)
465                 {
466                     // There are no sections defined, but RenderBody was NOT called.
467                     // If a body was defined, then RenderBody should have been called.
468                     throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_RenderBodyNotCalled, VirtualPath));
469                 }
470             }
471         }
472 
Write(HelperResult result)473         public override void Write(HelperResult result)
474         {
475             WriteTo(Output, result);
476         }
477 
Write(object value)478         public override void Write(object value)
479         {
480             WriteTo(Output, value);
481         }
482 
WriteLiteral(object value)483         public override void WriteLiteral(object value)
484         {
485             Output.Write(value);
486         }
487 
GetOutputWriter()488         protected internal override TextWriter GetOutputWriter()
489         {
490             return Output;
491         }
492     }
493 }
494