1 //
2 // System.Web.Compilation.AspParser
3 //
4 // Authors:
5 //	Gonzalo Paniagua Javier (gonzalo@ximian.com)
6 //      Marek Habersack <mhabersack@novell.com>
7 //
8 // (C) 2002,2003 Ximian, Inc (http://www.ximian.com)
9 // (C) 2004-2009 Novell, Inc (http://novell.com)
10 //
11 
12 //
13 // Permission is hereby granted, free of charge, to any person obtaining
14 // a copy of this software and associated documentation files (the
15 // "Software"), to deal in the Software without restriction, including
16 // without limitation the rights to use, copy, modify, merge, publish,
17 // distribute, sublicense, and/or sell copies of the Software, and to
18 // permit persons to whom the Software is furnished to do so, subject to
19 // the following conditions:
20 //
21 // The above copyright notice and this permission notice shall be
22 // included in all copies or substantial portions of the Software.
23 //
24 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
28 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 //
32 using System;
33 using System.ComponentModel;
34 using System.Collections;
35 using System.Globalization;
36 using System.IO;
37 using System.Text;
38 using System.Web.Util;
39 using System.Security.Cryptography;
40 
41 namespace System.Web.Compilation
42 {
ParseErrorHandler(ILocation location, string message)43 	delegate void ParseErrorHandler (ILocation location, string message);
TextParsedHandler(ILocation location, string text)44 	delegate void TextParsedHandler (ILocation location, string text);
TagParsedHandler(ILocation location, TagType tagtype, string id, TagAttributes attributes)45 	delegate void TagParsedHandler (ILocation location, TagType tagtype, string id, TagAttributes attributes);
ParsingCompleteHandler()46 	delegate void ParsingCompleteHandler ();
47 
48 	class AspParser : ILocation
49 	{
50 		static readonly object errorEvent = new object ();
51 		static readonly object tagParsedEvent = new object ();
52 		static readonly object textParsedEvent = new object ();
53 		static readonly object parsingCompleteEvent = new object();
54 
55 		MD5 checksum;
56 		AspTokenizer tokenizer;
57 		int beginLine, endLine;
58 		int beginColumn, endColumn;
59 		int beginPosition, endPosition;
60 		string filename;
61 		string verbatimID;
62 		string fileText;
63 		StringReader fileReader;
64 		bool _internal;
65 		int _internalLineOffset;
66 		int _internalPositionOffset;
67 		AspParser outer;
68 
69 		EventHandlerList events = new EventHandlerList ();
70 
71 		public event ParseErrorHandler Error {
72 			add { events.AddHandler (errorEvent, value); }
73 			remove { events.RemoveHandler (errorEvent, value); }
74 		}
75 
76 		public event TagParsedHandler TagParsed {
77 			add { events.AddHandler (tagParsedEvent, value); }
78 			remove { events.RemoveHandler (tagParsedEvent, value); }
79 		}
80 
81 		public event TextParsedHandler TextParsed {
82 			add { events.AddHandler (textParsedEvent, value); }
83 			remove { events.RemoveHandler (textParsedEvent, value); }
84 		}
85 
86 		public event ParsingCompleteHandler ParsingComplete {
87 			add { events.AddHandler (parsingCompleteEvent, value); }
88 			remove { events.RemoveHandler (parsingCompleteEvent, value); }
89 		}
90 
AspParser(string filename, TextReader input)91 		public AspParser (string filename, TextReader input)
92 		{
93 			this.filename = filename;
94 			this.fileText = input.ReadToEnd ();
95 			this.fileReader = new StringReader (this.fileText);
96 			this._internalLineOffset = 0;
97 			tokenizer = new AspTokenizer (this.fileReader);
98 		}
99 
AspParser(string filename, TextReader input, int startLineOffset, int positionOffset, AspParser outer)100 		public AspParser (string filename, TextReader input, int startLineOffset, int positionOffset, AspParser outer)
101 			: this (filename, input)
102 		{
103 			this._internal = true;
104 			this._internalLineOffset = startLineOffset;
105 			this._internalPositionOffset = positionOffset;
106 			this.outer = outer;
107 		}
108 
109 		public byte[] MD5Checksum {
110 			get {
111 				if (checksum == null)
112 					return new byte[0];
113 
114 				return checksum.Hash;
115 			}
116 		}
117 
118 		public int BeginPosition {
119 			get { return beginPosition; }
120 		}
121 
122 		public int EndPosition {
123 			get { return endPosition; }
124 		}
125 
126 		public int BeginLine {
127 			get {
128 				if (_internal)
129 					return beginLine + _internalLineOffset;
130 
131 				return beginLine;
132 			}
133 		}
134 
135 		public int BeginColumn {
136 			get { return beginColumn; }
137 		}
138 
139 		public int EndLine {
140 			get {
141 				if (_internal)
142 					return endLine + _internalLineOffset;
143 				return endLine;
144 			}
145 		}
146 
147 		public int EndColumn {
148 			get { return endColumn; }
149 		}
150 
151 		public string FileText {
152 			get {
153 				string ret = null;
154 
155 				if (_internal && outer != null)
156 				 	ret = outer.FileText;
157 
158 				if (ret == null && fileText != null)
159 					ret = fileText;
160 
161 				return ret;
162 			}
163 		}
164 
165 		public string PlainText {
166 			get {
167 				if (beginPosition >= endPosition || fileText == null)
168 					return null;
169 
170 				string text = FileText;
171 				int start, len;
172 
173 				if (_internal && outer != null) {
174 					start = beginPosition + _internalPositionOffset;
175 					len = (endPosition + _internalPositionOffset) - start;
176 				} else {
177 					start = beginPosition;
178 					len = endPosition - beginPosition;
179 				}
180 
181 				if (text != null)
182 					return text.Substring (start, len);
183 
184 				return null;
185 			}
186 		}
187 
188 		public string Filename {
189 			get {
190 				if (_internal && outer != null)
191 					return outer.Filename;
192 
193 				return filename;
194 			}
195 		}
196 
197 		public string VerbatimID {
198 			set {
199 				tokenizer.Verbatim = true;
200 				verbatimID = value;
201 			}
202 		}
203 
Eat(int expected_token)204 		bool Eat (int expected_token)
205 		{
206 			int token = tokenizer.get_token ();
207 			if (token != expected_token) {
208 				tokenizer.put_back ();
209 				return false;
210 			}
211 
212 			endLine = tokenizer.EndLine;
213 			endColumn = tokenizer.EndColumn;
214 			return true;
215 		}
216 
BeginElement()217 		void BeginElement ()
218 		{
219 			beginLine = tokenizer.BeginLine;
220 			beginColumn = tokenizer.BeginColumn;
221 			beginPosition = tokenizer.Position - 1;
222 		}
223 
EndElement()224 		void EndElement ()
225 		{
226 			endLine = tokenizer.EndLine;
227 			endColumn = tokenizer.EndColumn;
228 			endPosition = tokenizer.Position;
229 		}
230 
Parse()231 		public void Parse ()
232 		{
233 			if (tokenizer == null) {
234 				OnError ("AspParser not initialized properly.");
235 				return;
236 			}
237 
238 			int token;
239 			string id;
240 			TagAttributes attributes;
241 			TagType tagtype = TagType.Text;
242 			StringBuilder text =  new StringBuilder ();
243 
244 			try {
245 				while ((token = tokenizer.get_token ()) != Token.EOF) {
246 					BeginElement ();
247 
248 					if (tokenizer.Verbatim){
249 						string end_verbatim = "</" + verbatimID + ">";
250 						string verbatim_text = GetVerbatim (token, end_verbatim);
251 
252 						if (verbatim_text == null)
253 							OnError ("Unexpected EOF processing " + verbatimID);
254 
255 						tokenizer.Verbatim = false;
256 
257 						EndElement ();
258 						endPosition -= end_verbatim.Length;
259 						OnTextParsed (verbatim_text);
260 						beginPosition = endPosition;
261 						endPosition += end_verbatim.Length;
262 						OnTagParsed (TagType.Close, verbatimID, null);
263 						continue;
264 					}
265 
266 					if (token == '<') {
267 						GetTag (out tagtype, out id, out attributes);
268 						EndElement ();
269 						if (tagtype == TagType.ServerComment)
270 							continue;
271 
272 						if (tagtype == TagType.Text)
273 							OnTextParsed (id);
274 						else
275 							OnTagParsed (tagtype, id, attributes);
276 
277 						continue;
278 					}
279 
280 					if (tokenizer.Value.Trim ().Length == 0 && tagtype == TagType.Directive) {
281 						continue;
282 					}
283 
284 					text.Length = 0;
285 					do {
286 						text.Append (tokenizer.Value);
287 						token = tokenizer.get_token ();
288 					} while (token != '<' && token != Token.EOF);
289 
290 					tokenizer.put_back ();
291 					EndElement ();
292 					OnTextParsed (text.ToString ());
293 				}
294 			} finally {
295 				if (fileReader != null) {
296 					fileReader.Close ();
297 					fileReader = null;
298 				}
299 				checksum = tokenizer.Checksum;
300 				tokenizer = null;
301 			}
302 
303 			OnParsingComplete ();
304 		}
305 
GetInclude(string str, out string pathType, out string filename)306 		bool GetInclude (string str, out string pathType, out string filename)
307 		{
308 			pathType = null;
309 			filename = null;
310 			str = str.Substring (2).Trim ();
311 			int len = str.Length;
312 			int lastQuote = str.LastIndexOf ('"');
313 			if (len < 10 || lastQuote != len - 1)
314 				return false;
315 
316 			if (!StrUtils.StartsWith (str, "#include ", true))
317 				return false;
318 
319 			str = str.Substring (9).Trim ();
320 			bool isfile = (StrUtils.StartsWith (str ,"file", true));
321 			if (!isfile && !StrUtils.StartsWith (str, "virtual", true))
322 				return false;
323 
324 			pathType = (isfile) ? "file" : "virtual";
325 			if (str.Length < pathType.Length + 3)
326 				return false;
327 
328 			str = str.Substring (pathType.Length).Trim ();
329 			if (str.Length < 3 || str [0] != '=')
330 				return false;
331 
332 			int index = 1;
333 			for (; index < str.Length; index++) {
334 				if (Char.IsWhiteSpace (str [index]))
335 					continue;
336 				else if (str [index] == '"')
337 					break;
338 			}
339 
340 			if (index == str.Length || index == lastQuote)
341 				return false;
342 
343 			str = str.Substring (index);
344 			if (str.Length == 2) { // only quotes
345 				OnError ("Empty file name.");
346 				return false;
347 			}
348 
349 			filename = str.Trim ().Substring (index, str.Length - 2);
350 			if (filename.LastIndexOf  ('"') != -1)
351 				return false; // file=""" -> no error
352 
353 			return true;
354 		}
355 
GetTag(out TagType tagtype, out string id, out TagAttributes attributes)356 		void GetTag (out TagType tagtype, out string id, out TagAttributes attributes)
357 		{
358 			int token = tokenizer.get_token ();
359 
360 			tagtype = TagType.ServerComment;
361 			id = null;
362 			attributes = null;
363 			switch (token){
364 			case '%':
365 				GetServerTag (out tagtype, out id, out attributes);
366 				break;
367 			case '/':
368 				if (!Eat (Token.IDENTIFIER))
369 					OnError ("expecting TAGNAME");
370 
371 				id = tokenizer.Value;
372 				if (!Eat ('>'))
373 					OnError ("expecting '>'. Got '" + id + "'");
374 
375 				tagtype = TagType.Close;
376 				break;
377 			case '!':
378 				bool double_dash = Eat (Token.DOUBLEDASH);
379 				if (double_dash)
380 					tokenizer.put_back ();
381 
382 				tokenizer.Verbatim = true;
383 				string end = double_dash ? "-->" : ">";
384 				string comment = GetVerbatim (tokenizer.get_token (), end);
385 				tokenizer.Verbatim = false;
386 				if (comment == null)
387 					OnError ("Unfinished HTML comment/DTD");
388 
389 				string pathType, filename;
390 				if (double_dash && GetInclude (comment, out pathType, out filename)) {
391 					tagtype = TagType.Include;
392 					attributes = new TagAttributes ();
393 					attributes.Add (pathType, filename);
394 				} else {
395 					tagtype = TagType.Text;
396 					id = "<!" + comment + end;
397 				}
398 				break;
399 			case Token.IDENTIFIER:
400 				if (this.filename == "@@inner_string@@") {
401 					// Actually not tag but "xxx < yyy" stuff in inner_string!
402 					tagtype = TagType.Text;
403 					tokenizer.InTag = false;
404 					id = "<" + tokenizer.Odds + tokenizer.Value;
405 				} else {
406 					id = tokenizer.Value;
407 					try {
408 						attributes = GetAttributes ();
409 					} catch (Exception e) {
410 						OnError (e.Message);
411 						break;
412 					}
413 
414 					tagtype = TagType.Tag;
415 					if (Eat ('/') && Eat ('>')) {
416 						tagtype = TagType.SelfClosing;
417 					} else if (!Eat ('>')) {
418 						if (attributes.IsRunAtServer ()) {
419 							OnError ("The server tag is not well formed.");
420 							break;
421 						}
422 						tokenizer.Verbatim = true;
423 						attributes.Add (String.Empty, GetVerbatim (tokenizer.get_token (), ">") + ">");
424 						tokenizer.Verbatim = false;
425 					}
426 				}
427 
428 				break;
429 			default:
430 				string idvalue = null;
431 				// This is to handle code like:
432 				//
433 				//  <asp:ListItem runat="server"> < </asp:ListItem>
434 				//
435 				if ((char)token == '<') {
436 					string odds = tokenizer.Odds;
437 					if (odds != null && odds.Length > 0 && Char.IsWhiteSpace (odds [0])) {
438 						tokenizer.put_back ();
439 						idvalue = odds;
440 					} else
441 						idvalue = tokenizer.Value;
442 				} else
443 					idvalue = tokenizer.Value;
444 
445 				tagtype = TagType.Text;
446 				tokenizer.InTag = false;
447 				id = "<" + idvalue;
448 				break;
449 			}
450 		}
451 
GetAttributes()452 		TagAttributes GetAttributes ()
453 		{
454 			int token;
455 			TagAttributes attributes;
456 			string id;
457 			bool wellFormedForServer = true;
458 
459 			attributes = new TagAttributes ();
460 			while ((token = tokenizer.get_token ()) != Token.EOF){
461 				if (token == '<' && Eat ('%')) {
462 					tokenizer.Verbatim = true;
463 					attributes.Add (String.Empty, "<%" +
464 							GetVerbatim (tokenizer.get_token (), "%>") + "%>");
465 					tokenizer.Verbatim = false;
466 					tokenizer.InTag = true;
467 					continue;
468 				}
469 
470 				if (token != Token.IDENTIFIER)
471 					break;
472 
473 				id = tokenizer.Value;
474 				if (Eat ('=')){
475 					if (Eat (Token.ATTVALUE)){
476 						attributes.Add (id, tokenizer.Value);
477 						wellFormedForServer &= tokenizer.AlternatingQuotes;
478 					} else if (Eat ('<') && Eat ('%')) {
479 						tokenizer.Verbatim = true;
480 						attributes.Add (id, "<%" +
481 								GetVerbatim (tokenizer.get_token (), "%>") + "%>");
482 						tokenizer.Verbatim = false;
483 						tokenizer.InTag = true;
484 					} else {
485 						OnError ("expected ATTVALUE");
486 						return null;
487 					}
488 				} else {
489 					attributes.Add (id, null);
490 				}
491 			}
492 
493 			tokenizer.put_back ();
494 
495 			if (attributes.IsRunAtServer () && !wellFormedForServer) {
496 				OnError ("The server tag is not well formed.");
497 				return null;
498 			}
499 
500 			return attributes;
501 		}
502 
GetVerbatim(int token, string end)503 		string GetVerbatim (int token, string end)
504 		{
505 			StringBuilder vb_text = new StringBuilder ();
506 			StringBuilder tmp = new StringBuilder ();
507 			int i = 0;
508 
509 			if (tokenizer.Value.Length > 1){
510 				// May be we have a put_back token that is not a single character
511 				vb_text.Append (tokenizer.Value);
512 				token = tokenizer.get_token ();
513 			}
514 
515 			end = end.ToLower (Helpers.InvariantCulture);
516 			int repeated = 0;
517 			for (int k = 0; k < end.Length; k++)
518 				if (end [0] == end [k])
519 					repeated++;
520 
521 			while (token != Token.EOF){
522 				if (Char.ToLower ((char) token, Helpers.InvariantCulture) == end [i]){
523 					if (++i >= end.Length)
524 						break;
525 					tmp.Append ((char) token);
526 					token = tokenizer.get_token ();
527 					continue;
528 				} else if (i > 0) {
529 					if (repeated > 1 && i == repeated && (char) token == end [0]) {
530 						vb_text.Append ((char) token);
531 						token = tokenizer.get_token ();
532 						continue;
533 					}
534 					vb_text.Append (tmp.ToString ());
535 					tmp.Remove (0, tmp.Length);
536 					i = 0;
537 				}
538 
539 				vb_text.Append ((char) token);
540 				token = tokenizer.get_token ();
541 			}
542 
543 			if (token == Token.EOF)
544 				OnError ("Expecting " + end + " and got EOF.");
545 
546 			return RemoveComments (vb_text.ToString ());
547 		}
548 
RemoveComments(string text)549 		string RemoveComments (string text)
550 		{
551 			int end;
552 			int start = text.IndexOf ("<%--");
553 
554 			while (start != -1) {
555 				end = text.IndexOf ("--%>");
556 				if (end == -1 || end <= start + 1)
557 					break;
558 
559 				text = text.Remove (start, end - start + 4);
560 				start = text.IndexOf ("<%--");
561 			}
562 
563 			return text;
564 		}
565 
GetServerTag(out TagType tagtype, out string id, out TagAttributes attributes)566 		void GetServerTag (out TagType tagtype, out string id, out TagAttributes attributes)
567 		{
568 			string inside_tags;
569 			bool old = tokenizer.ExpectAttrValue;
570 
571 			tokenizer.ExpectAttrValue = false;
572 			if (Eat ('@')){
573 				tokenizer.ExpectAttrValue = old;
574 				tagtype = TagType.Directive;
575 				id = "";
576 				if (Eat (Token.DIRECTIVE))
577 					id = tokenizer.Value;
578 
579 				attributes = GetAttributes ();
580 				if (!Eat ('%') || !Eat ('>'))
581 					OnError ("expecting '%>'");
582 
583 				return;
584 			}
585 
586 			if (Eat (Token.DOUBLEDASH)) {
587 				tokenizer.ExpectAttrValue = old;
588 				tokenizer.Verbatim = true;
589 				inside_tags = GetVerbatim (tokenizer.get_token (), "--%>");
590 				tokenizer.Verbatim = false;
591 				id = null;
592 				attributes = null;
593 				tagtype = TagType.ServerComment;
594 				return;
595 			}
596 
597 			tokenizer.ExpectAttrValue = old;
598 			bool varname;
599 			bool databinding;
600 			bool codeRenderEncode;
601 			varname = Eat ('=');
602 			databinding = !varname && Eat ('#');
603 			codeRenderEncode = !databinding && !varname && Eat (':');
604 			string odds = tokenizer.Odds;
605 
606 			tokenizer.Verbatim = true;
607 			inside_tags = GetVerbatim (tokenizer.get_token (), "%>");
608 			if (databinding && odds != null && odds.Length > 0) {
609 				databinding = false;
610 
611 				// We encountered <% #something here %>, this should be passed
612 				// verbatim to the compiler
613 				inside_tags = '#' + inside_tags;
614 			}
615 
616 			tokenizer.Verbatim = false;
617 			id = inside_tags;
618 			attributes = null;
619 			if (databinding)
620 				tagtype = TagType.DataBinding;
621 			else if (varname)
622 				tagtype = TagType.CodeRenderExpression;
623 			else if (codeRenderEncode)
624 				tagtype = TagType.CodeRenderEncode;
625 			else
626 				tagtype = TagType.CodeRender;
627 		}
628 
ToString()629 		public override string ToString ()
630 		{
631 			StringBuilder sb = new StringBuilder ("AspParser {");
632 			if (filename != null && filename.Length > 0)
633 				sb.AppendFormat ("{0}:{1}.{2}", filename, beginLine, beginColumn);
634 			sb.Append ('}');
635 
636 			return sb.ToString ();
637 		}
638 
OnError(string msg)639 		void OnError (string msg)
640 		{
641 			ParseErrorHandler eh = events [errorEvent] as ParseErrorHandler;
642 			if (eh != null)
643 				eh (this, msg);
644 		}
645 
OnTagParsed(TagType tagtype, string id, TagAttributes attributes)646 		void OnTagParsed (TagType tagtype, string id, TagAttributes attributes)
647 		{
648 			TagParsedHandler eh = events [tagParsedEvent] as TagParsedHandler;
649 			if (eh != null)
650 				eh (this, tagtype, id, attributes);
651 		}
652 
OnTextParsed(string text)653 		void OnTextParsed (string text)
654 		{
655 			TextParsedHandler eh = events [textParsedEvent] as TextParsedHandler;
656 			if (eh != null)
657 				eh (this, text);
658 		}
659 
OnParsingComplete()660 		void OnParsingComplete ()
661 		{
662 			ParsingCompleteHandler eh = events [parsingCompleteEvent] as ParsingCompleteHandler;
663 			if (eh != null)
664 				eh ();
665 		}
666 	}
667 }
668 
669