1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using System.Collections.Specialized;
5 using System.Configuration;
6 using System.IO;
7 using System.Linq;
8 using System.Reflection;
9 using System.Runtime.InteropServices;
10 using System.Xml;
11 
12 using Monodoc.Providers;
13 using Lucene.Net.Analysis.Standard;
14 using Lucene.Net.Index;
15 
16 namespace Monodoc
17 {
18 	public
19 #if LEGACY_MODE
20 	partial
21 #endif
22 	class RootTree : Tree
23 	{
24 		public const int MonodocVersion = 2;
25 		const string RootNamespace = "root:/";
26 		string basedir;
27 		static List<string> uncompiledHelpSourcePaths = new List<string>();
28 		HashSet<string> loadedSourceFiles = new HashSet<string>();
29 		List<HelpSource> helpSources = new List<HelpSource>();
30 		Dictionary<string, Node> nameToNode = new Dictionary<string, Node>();
31 		Dictionary<string, HelpSource> nameToHelpSource = new Dictionary<string, HelpSource>();
32 
33 		public IList<HelpSource> HelpSources {
34 			get {
35 				return this.helpSources.AsReadOnly();
36 			}
37 		}
38 
39 		public DateTime LastHelpSourceTime {
40 			get;
41 			set;
42 		}
43 
44 		static bool IsUnix {
45 			get {
46 				int platform = (int)Environment.OSVersion.Platform;
47 				return platform == 4 || platform == 128 || platform == 6;
48 			}
49 		}
50 
RootTree()51 		RootTree () : base (null, "Mono Documentation", "root:")
52 		{
53 			base.RootNode.EnsureNodes();
54 			this.LastHelpSourceTime = DateTime.Now;
55 		}
56 
AddUncompiledSource(string path)57 		public static void AddUncompiledSource (string path)
58 		{
59 			uncompiledHelpSourcePaths.Add (path);
60 		}
61 
LoadTree()62 		public static RootTree LoadTree ()
63 		{
64 			return RootTree.LoadTree (RootTree.ProbeBaseDirectories ());
65 		}
66 
ProbeBaseDirectories()67 		static string ProbeBaseDirectories ()
68 		{
69 			string result = ".";
70 			try {
71 				result = Config.Get ("docPath") ?? ".";
72 			} catch {}
73 
74 			return result;
75 		}
76 
LoadTree(string basedir, bool includeExternal = true)77 		public static RootTree LoadTree (string basedir, bool includeExternal = true)
78 		{
79 			if (string.IsNullOrEmpty (basedir))
80 				throw new ArgumentNullException ("basedir");
81 			if (!Directory.Exists (basedir))
82 				throw new ArgumentException ("basedir", string.Format ("Base documentation directory at '{0}' doesn't exist", basedir));
83 
84 			XmlDocument xmlDocument = new XmlDocument ();
85 			string filename = Path.Combine (basedir, "monodoc.xml");
86 			xmlDocument.Load (filename);
87 			IEnumerable<string> sourceFiles = Directory.EnumerateFiles (Path.Combine (basedir, "sources"), "*.source");
88 			if (includeExternal)
89 				sourceFiles = sourceFiles.Concat (RootTree.ProbeExternalDirectorySources ());
90 			return RootTree.LoadTree (basedir, xmlDocument, sourceFiles);
91 		}
92 
ProbeExternalDirectorySources()93 		static IEnumerable<string> ProbeExternalDirectorySources ()
94 		{
95 			IEnumerable<string> enumerable = Enumerable.Empty<string> ();
96 			try {
97 				string path = Config.Get ("docExternalPath");
98 				enumerable = enumerable.Concat (System.IO.Directory.EnumerateFiles (path, "*.source"));
99 			}
100 			catch {}
101 
102 			if (Directory.Exists ("/Library/Frameworks/Mono.framework/External/monodoc"))
103 				enumerable = enumerable.Concat (Directory.EnumerateFiles ("/Library/Frameworks/Mono.framework/External/monodoc", "*.source"));
104 
105 			var windowsPath = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData), "monodoc");
106 			if (Directory.Exists (windowsPath))
107 				enumerable = enumerable.Concat (Directory.EnumerateFiles (windowsPath, "*.source"));
108 
109 			return enumerable;
110 		}
111 
LoadTree(string indexDir, XmlDocument docTree, IEnumerable<string> sourceFiles)112 		public static RootTree LoadTree (string indexDir, XmlDocument docTree, IEnumerable<string> sourceFiles)
113 		{
114 			if (docTree == null) {
115 				docTree = new XmlDocument ();
116 				using  (Stream manifestResourceStream = typeof (RootTree).Assembly.GetManifestResourceStream ("monodoc.xml")) {
117 					docTree.Load (manifestResourceStream);
118 				}
119 			}
120 
121 			sourceFiles =  (sourceFiles ?? new string[0]);
122 			RootTree rootTree = new RootTree ();
123 			rootTree.basedir = indexDir;
124 			XmlNodeList xml_node_list = docTree.SelectNodes ("/node/node");
125 			rootTree.nameToNode["root"] = rootTree.RootNode;
126 			rootTree.nameToNode["libraries"] = rootTree.RootNode;
127 			rootTree.Populate (rootTree.RootNode, xml_node_list);
128 
129 			if (rootTree.LookupEntryPoint ("various") == null) {
130 				Console.Error.WriteLine ("No 'various' doc node! Check monodoc.xml!");
131 				Node rootNode = rootTree.RootNode;
132 			}
133 
134 			foreach (string current in sourceFiles)
135 				rootTree.AddSourceFile (current);
136 
137 			foreach (string path in uncompiledHelpSourcePaths) {
138 				var hs = new Providers.EcmaUncompiledHelpSource (path);
139 				hs.RootTree = rootTree;
140 				rootTree.helpSources.Add (hs);
141 				string epath = "extra-help-source-" + hs.Name;
142 				Node hsn = rootTree.RootNode.CreateNode (hs.Name, RootNamespace + epath);
143 				rootTree.nameToHelpSource [epath] = hs;
144 				hsn.EnsureNodes ();
145 				foreach (Node n in hs.Tree.RootNode.ChildNodes)
146 					hsn.AddNode (n);
147 			}
148 
149 			RootTree.PurgeNode (rootTree.RootNode);
150 			rootTree.RootNode.Sort ();
151 			return rootTree;
152 		}
153 
AddSource(string sourcesDir)154 		public void AddSource (string sourcesDir)
155 		{
156 			IEnumerable<string> enumerable = Directory.EnumerateFiles (sourcesDir, "*.source");
157 			foreach (string current in enumerable)
158 				if (!this.AddSourceFile (current))
159 					Console.Error.WriteLine ("Error: Could not load source file {0}", current);
160 		}
161 
AddSourceFile(string sourceFile)162 		public bool AddSourceFile (string sourceFile)
163 		{
164 			if (this.loadedSourceFiles.Contains (sourceFile))
165 				return false;
166 
167 			Node node = this.LookupEntryPoint ("various") ?? base.RootNode;
168 			XmlDocument xmlDocument = new XmlDocument ();
169 			try {
170 				xmlDocument.Load (sourceFile);
171 			} catch {
172 				bool result = false;
173 				return result;
174 			}
175 
176 			XmlNodeList extra_nodes = xmlDocument.SelectNodes ("/monodoc/node");
177 			if (extra_nodes.Count > 0)
178 				this.Populate (node, extra_nodes);
179 
180 			XmlNodeList sources = xmlDocument.SelectNodes ("/monodoc/source");
181 			if (sources == null) {
182 				Console.Error.WriteLine ("Error: No <source> section found in the {0} file", sourceFile);
183 				return false;
184 			}
185 
186 			loadedSourceFiles.Add (sourceFile);
187 			foreach (XmlNode xmlNode in sources) {
188 				XmlAttribute a = xmlNode.Attributes["provider"];
189 				if (a == null) {
190 					Console.Error.WriteLine ("Error: no provider in <source>");
191 					continue;
192 				}
193 				string provider = a.InnerText;
194 				a = xmlNode.Attributes["basefile"];
195 				if (a == null) {
196 					Console.Error.WriteLine ("Error: no basefile in <source>");
197 					continue;
198 				}
199 				string basefile = a.InnerText;
200 				a = xmlNode.Attributes["path"];
201 				if (a == null) {
202 					Console.Error.WriteLine ("Error: no path in <source>");
203 					continue;
204 				}
205 				string path = a.InnerText;
206 				string basefilepath = Path.Combine (Path.GetDirectoryName (sourceFile), basefile);
207 				HelpSource helpSource = RootTree.GetHelpSource (provider, basefilepath);
208 				if (helpSource != null) {
209 					helpSource.RootTree = this;
210 					this.helpSources.Add (helpSource);
211 					this.nameToHelpSource[path] = helpSource;
212 					Node node2 = this.LookupEntryPoint (path);
213 					if (node2 == null) {
214 						Console.Error.WriteLine ("node `{0}' is not defined on the documentation map", path);
215 						node2 = node;
216 					}
217 					foreach (Node current in helpSource.Tree.RootNode.ChildNodes) {
218 						node2.AddNode (current);
219 					}
220 					node2.Sort ();
221 				}
222 			}
223 			return true;
224 		}
225 
PurgeNode(Node node)226 		static bool PurgeNode (Node node)
227 		{
228 			bool result = false;
229 			if (!node.Documented)
230 			{
231 				List<Node> list = new List<Node> ();
232 				foreach (Node current in node.ChildNodes)
233 				{
234 					bool flag = RootTree.PurgeNode (current);
235 					if (flag)
236 					{
237 						list.Add (current);
238 					}
239 				}
240 				result =  (node.ChildNodes.Count == list.Count);
241 				foreach (Node current2 in list)
242 				{
243 					node.DeleteNode (current2);
244 				}
245 			}
246 			return result;
247 		}
248 
GetSupportedFormats()249 		public static string[] GetSupportedFormats ()
250 		{
251 			return new string[]
252 			{
253 				"ecma",
254 				"ecmaspec",
255 				"error",
256 				"man",
257 				"xhtml"
258 			};
259 		}
260 
GetHelpSource(string provider, string basefilepath)261 		public static HelpSource GetHelpSource (string provider, string basefilepath)
262 		{
263 			HelpSource result;
264 			try {
265 				switch (provider) {
266 				case "xhtml":
267 				case "hb":
268 					result = new XhtmlHelpSource (basefilepath, false);
269 					break;
270 				case "man":
271 					result = new ManHelpSource (basefilepath, false);
272 					break;
273 				case "error":
274 					result = new ErrorHelpSource (basefilepath, false);
275 					break;
276 				case "ecmaspec":
277 					result = new EcmaSpecHelpSource (basefilepath, false);
278 					break;
279 				case "ecma":
280 					result = new EcmaHelpSource (basefilepath, false);
281 					break;
282 				default:
283 					Console.Error.WriteLine ("Error: Unknown provider specified: {0}", provider);
284 					result = null;
285 					break;
286 				}
287 			} catch (FileNotFoundException) {
288 				Console.Error.WriteLine ("Error: did not find one of the files in sources/" + basefilepath);
289 				result = null;
290 			}
291 			return result;
292 		}
293 
GetProvider(string provider, params string[] basefilepaths)294 		public static Provider GetProvider (string provider, params string[] basefilepaths)
295 		{
296 			switch (provider) {
297 			case "ecma":
298 				return new EcmaProvider (basefilepaths[0]);
299 			case "ecmaspec":
300 				return new EcmaSpecProvider (basefilepaths[0]);
301 			case "error":
302 				return new ErrorProvider (basefilepaths[0]);
303 			case "man":
304 				return new ManProvider (basefilepaths);
305 			case "xhml":
306 			case "hb":
307 				return new XhtmlProvider (basefilepaths[0]);
308 			}
309 
310 			throw new NotSupportedException (provider);
311 		}
312 
Populate(Node parent, XmlNodeList xml_node_list)313 		void Populate (Node parent, XmlNodeList xml_node_list)
314 		{
315 			foreach (XmlNode xmlNode in xml_node_list) {
316 				XmlAttribute e = xmlNode.Attributes["parent"];
317 				Node parent2 = null;
318 				if (e != null && this.nameToNode.TryGetValue (e.InnerText, out parent2)) {
319 					xmlNode.Attributes.Remove (e);
320 					Populate (parent2, xmlNode.SelectNodes ("."));
321 					continue;
322 				}
323 				e = xmlNode.Attributes["label"];
324 				if (e == null) {
325 					Console.Error.WriteLine ("`label' attribute missing in <node>");
326 					continue;
327 				}
328 				string label = e.InnerText;
329 				e = xmlNode.Attributes["name"];
330 				if (e == null) {
331 					Console.Error.WriteLine ("`name' attribute missing in <node>");
332 					continue;
333 				}
334 				string name = e.InnerText;
335 				Node orCreateNode = parent.GetOrCreateNode (label, RootNamespace + name);
336 				orCreateNode.EnsureNodes ();
337 				this.nameToNode[name] = orCreateNode;
338 				XmlNodeList xmlNodeList = xmlNode.SelectNodes ("./node");
339 				if (xmlNodeList != null) {
340 					this.Populate (orCreateNode, xmlNodeList);
341 				}
342 			}
343 		}
344 
LookupEntryPoint(string name)345 		public Node LookupEntryPoint (string name)
346 		{
347 			Node result = null;
348 			if (!this.nameToNode.TryGetValue (name, out result))
349 				result = null;
350 			return result;
351 		}
352 
RenderUrl(string url, IDocGenerator<TOutput> generator, HelpSource hintSource = null)353 		public TOutput RenderUrl<TOutput> (string url, IDocGenerator<TOutput> generator, HelpSource hintSource = null)
354 		{
355 			Node dummy;
356 			return RenderUrl<TOutput> (url, generator, out dummy, hintSource);
357 		}
358 
RenderUrl(string url, IDocGenerator<TOutput> generator, out Node node, HelpSource hintSource = null)359 		public TOutput RenderUrl<TOutput> (string url, IDocGenerator<TOutput> generator, out Node node, HelpSource hintSource = null)
360 		{
361 			node = null;
362 			string internalId = null;
363 			Dictionary<string, string> context = null;
364 			HelpSource hs = GetHelpSourceAndIdForUrl (url, hintSource, out internalId, out context, out node);
365 			return generator.Generate (hs, internalId, context);
366 		}
367 
GetHelpSourceAndIdForUrl(string url, out string internalId, out Dictionary<string, string> context)368 		public HelpSource GetHelpSourceAndIdForUrl (string url, out string internalId, out Dictionary<string, string> context)
369 		{
370 			Node dummy;
371 			return GetHelpSourceAndIdForUrl (url, out internalId, out context, out dummy);
372 		}
373 
GetHelpSourceAndIdForUrl(string url, out string internalId, out Dictionary<string, string> context, out Node node)374 		public HelpSource GetHelpSourceAndIdForUrl (string url, out string internalId, out Dictionary<string, string> context, out Node node)
375 		{
376 			return GetHelpSourceAndIdForUrl (url, null, out internalId, out context, out node);
377 		}
378 
GetHelpSourceAndIdForUrl(string url, HelpSource hintSource, out string internalId, out Dictionary<string, string> context, out Node node)379 		public HelpSource GetHelpSourceAndIdForUrl (string url, HelpSource hintSource, out string internalId, out Dictionary<string, string> context, out Node node)
380 		{
381 			node = null;
382 			internalId = null;
383 			context = null;
384 
385 			if (url == "root:") {
386 				context = new Dictionary<string, string> { {"specialpage", "master-root"} };
387 				internalId = url;
388 				node = null;
389 				// We return the first help source available since the generator will simply fetch this RootTree instance through it
390 				return helpSources.FirstOrDefault ();
391 			}
392 			if (url.StartsWith (RootNamespace, StringComparison.OrdinalIgnoreCase)) {
393 				context = new Dictionary<string, string> { {"specialpage", "root"} };
394 				return GetHelpSourceAndIdFromName (url.Substring (RootNamespace.Length), out internalId, out node);
395 			}
396 
397 			HelpSource helpSource = hintSource;
398 			if (helpSource == null || string.IsNullOrEmpty (internalId = helpSource.GetInternalIdForUrl (url, out node, out context))) {
399 				helpSource = null;
400 				foreach (var hs in helpSources.Where (h => h.CanHandleUrl (url))) {
401 					if (!string.IsNullOrEmpty (internalId = hs.GetInternalIdForUrl (url, out node, out context))) {
402 						helpSource = hs;
403 						break;
404 					}
405 				}
406 			}
407 
408 			return helpSource;
409 		}
410 
GetHelpSourceAndIdFromName(string name, out string internalId, out Node node)411 		public HelpSource GetHelpSourceAndIdFromName (string name, out string internalId, out Node node)
412 		{
413 			internalId = "root:";
414 			node = LookupEntryPoint (name);
415 
416 			return node == null ? null : node.ChildNodes.Select (n => n.Tree.HelpSource).FirstOrDefault (hs => hs != null);
417 		}
418 
GetHelpSourceFromId(int id)419 		public HelpSource GetHelpSourceFromId (int id)
420 		{
421 			return  (id < 0 || id >= this.helpSources.Count) ? null : this.helpSources[id];
422 		}
423 
GetImage(string url)424 		public Stream GetImage (string url)
425 		{
426 			if (url.StartsWith ("source-id:", StringComparison.OrdinalIgnoreCase)) {
427 				string text = url.Substring (10);
428 				int num = text.IndexOf (":");
429 				string text2 = text.Substring (0, num);
430 				int id = 0;
431 				if (!int.TryParse (text2, out id)) {
432 					Console.Error.WriteLine ("Failed to parse source-id url: {0} `{1}'", url, text2);
433 					return null;
434 				}
435 				HelpSource helpSourceFromId = this.GetHelpSourceFromId (id);
436 				return helpSourceFromId.GetImage (text.Substring (num + 1));
437 			}
438 			Assembly assembly = Assembly.GetAssembly (typeof (RootTree));
439 			return assembly.GetManifestResourceStream (url);
440 		}
441 
GetIndex()442 		public IndexReader GetIndex ()
443 		{
444 			var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "monodoc.index"));
445 			var p = paths.FirstOrDefault (File.Exists);
446 			return p == null ? (IndexReader)null : IndexReader.Load (p);
447 		}
448 
MakeIndex()449 		public static void MakeIndex ()
450 		{
451 			RootTree rootTree = RootTree.LoadTree ();
452 			rootTree.GenerateIndex ();
453 		}
454 
GenerateIndex()455 		public bool GenerateIndex ()
456 		{
457 			IndexMaker indexMaker = new IndexMaker ();
458 			foreach (HelpSource current in this.helpSources)
459 				current.PopulateIndex (indexMaker);
460 
461 			var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "monodoc.index"));
462 			bool successful = false;
463 
464 			foreach (var path in paths) {
465 				try {
466 					indexMaker.Save (path);
467 					successful = true;
468 					if (RootTree.IsUnix)
469 						RootTree.chmod (path, 420);
470 				} catch (UnauthorizedAccessException) {
471 				}
472 			}
473 			if (!successful) {
474 				Console.WriteLine ("You don't have permissions to write on any of [" + string.Join (", ", paths) + "]");
475 				return false;
476 			}
477 
478 			Console.WriteLine ("Documentation index updated");
479 			return true;
480 		}
481 
GetSearchIndex()482 		public SearchableIndex GetSearchIndex ()
483 		{
484 			var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "search_index"));
485 			var p = paths.FirstOrDefault (Directory.Exists);
486 			return p == null ? (SearchableIndex)null : SearchableIndex.Load (p);
487 		}
488 
MakeSearchIndex()489 		public static void MakeSearchIndex ()
490 		{
491 			RootTree rootTree = RootTree.LoadTree ();
492 			rootTree.GenerateSearchIndex ();
493 		}
494 
GenerateSearchIndex()495 		public bool GenerateSearchIndex ()
496 		{
497 			Console.WriteLine ("Loading the monodoc tree...");
498 			IndexWriter indexWriter = null;
499 			var analyzer = new StandardAnalyzer (Lucene.Net.Util.Version.LUCENE_CURRENT);
500 			var paths = GetIndexesPathPrefixes ().Select (bp => Path.Combine (bp, "search_index"));
501 			bool successful = false;
502 
503 			foreach (var path in paths) {
504 				try {
505 					if (!Directory.Exists (path))
506 						Directory.CreateDirectory (path);
507 					var directory = Lucene.Net.Store.FSDirectory.Open (path);
508 					indexWriter = new IndexWriter (directory, analyzer, true, IndexWriter.MaxFieldLength.LIMITED);
509 					successful = true;
510 				} catch (UnauthorizedAccessException) {}
511 			}
512 			if (!successful) {
513 				Console.WriteLine ("You don't have permissions to write on any of [" + string.Join (", ", paths) + "]");
514 				return false;
515 			}
516 			Console.WriteLine ("Collecting and adding documents...");
517 			foreach (HelpSource current in this.helpSources) {
518 				current.PopulateSearchableIndex (indexWriter);
519 			}
520 			Console.WriteLine ("Closing...");
521 			indexWriter.Optimize ();
522 			indexWriter.Close ();
523 			return true;
524 		}
525 
526 		[DllImport ("libc")]
chmod(string filename, int mode)527 		static extern int chmod (string filename, int mode);
528 
GetIndexesPathPrefixes()529 		IEnumerable<string> GetIndexesPathPrefixes ()
530 		{
531 			yield return basedir;
532 			yield return Config.Get ("docPath");
533 			var indexDirectory = Config.Get ("monodocIndexDirectory");
534 			if (!string.IsNullOrEmpty (indexDirectory))
535 				yield return indexDirectory;
536 			yield return Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData), "monodoc");
537 		}
538 
539 		[Obsolete]
GetTitle(string url)540 		public string GetTitle (string url)
541 		{
542 			return "Mono Documentation";
543 		}
544 	}
545 }
546