PageRenderTime 48ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/Server/ObjectCloud.Disk.WebHandlers/WebHandler.cs

https://bitbucket.org/gwbasic/objectcloud
C# | 1462 lines | 892 code | 247 blank | 323 comment | 134 complexity | f80d73331dbe576e20f3a51bbbd1fe73 MD5 | raw file
Possible License(s): JSON, LGPL-2.1, MPL-2.0-no-copyleft-exception

Large files files are truncated, but you can click here to view the full file

  1. // Copyright 2009 Andrew Rondeau
  2. // This code is released under the LGPL license
  3. // For more information, see either DefaultFiles/Docs/license.wchtml or /Docs/license.wchtml
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Globalization;
  7. using System.Reflection;
  8. using System.Security.Cryptography;
  9. using System.Text;
  10. using System.Threading;
  11. using Common.Logging;
  12. using JsonFx.Json;
  13. using ObjectCloud.Common;
  14. using ObjectCloud.Interfaces.Disk;
  15. using ObjectCloud.Interfaces.Javascript;
  16. using ObjectCloud.Interfaces.Security;
  17. using ObjectCloud.Interfaces.WebServer;
  18. namespace ObjectCloud.Disk.WebHandlers
  19. {
  20. /// <summary>
  21. /// Generic web handler. Other web handlers can inherit from this web handler, or the Non-Generic WebHandler can be used
  22. /// if the file doesn't support web access
  23. /// </summary>
  24. /// <typeparam name="TFileHandler">The type of file handler</typeparam>
  25. public partial class WebHandler<TFileHandler> : IWebHandler
  26. where TFileHandler : IFileHandler
  27. {
  28. private static ILog log = LogManager.GetLogger(typeof(WebHandler<>));
  29. /// <summary>
  30. /// Returns a delegate to handle the incoming request
  31. /// </summary>
  32. /// <param name="webConnection"></param>
  33. /// <returns></returns>
  34. public virtual WebDelegate GetMethod(IWebConnection webConnection)
  35. {
  36. IExecutionEnvironment executionEnvironment = GetOrCreateExecutionEnvironment();
  37. bool allowLocalMethods = true;
  38. if (!webConnection.BypassJavascript)
  39. if (null != executionEnvironment)
  40. {
  41. WebDelegate toReturn = executionEnvironment.GetMethod(webConnection);
  42. if (null != toReturn)
  43. return toReturn;
  44. else
  45. allowLocalMethods = !executionEnvironment.IsBlockWebMethodsEnabled(webConnection);
  46. }
  47. string method = webConnection.GetArgumentOrException("Method");
  48. // When the call is local or there is no execution environment, then look for the base web method
  49. if (webConnection.CallingFrom == CallingFrom.Local || allowLocalMethods || AllowedBaseMethods.Contains(method))
  50. return FileHandlerFactoryLocator.WebMethodCache[MethodNameAndFileContainer.New(method, FileContainer)];
  51. // Throw an exception if no method is found
  52. throw new WebResultsOverrideException(WebResults.FromString(Status._400_Bad_Request, "method \"" + method + "\" does not exist"));
  53. }
  54. /// <summary>
  55. /// These methods are allowed even if the object is wrapped by a server-side javascript class
  56. /// </summary>
  57. private static Set<string> AllowedBaseMethods = new Set<string>();
  58. static WebHandler()
  59. {
  60. // Allow any base method to be called from the web
  61. foreach (MethodInfo method in typeof(WebHandler).GetMethods(BindingFlags.Instance | BindingFlags.Public))
  62. {
  63. if (typeof(IWebResults) == method.ReturnType)
  64. {
  65. ParameterInfo[] parms = method.GetParameters();
  66. if (parms.Length > 0)
  67. if (typeof(IWebConnection) == parms[0].ParameterType)
  68. AllowedBaseMethods.Add(method.Name);
  69. }
  70. }
  71. }
  72. /// <value>
  73. /// The FileHandler, pre-casted
  74. /// </value>
  75. public TFileHandler FileHandler
  76. {
  77. get { return _FileHandler; }
  78. }
  79. private TFileHandler _FileHandler;
  80. /// <value>
  81. /// The FileContainer
  82. /// </value>
  83. public IFileContainer FileContainer
  84. {
  85. get { return _FileContainer; }
  86. set
  87. {
  88. _FileContainer = value;
  89. _FileHandler = value.CastFileHandler<TFileHandler>();
  90. }
  91. }
  92. private IFileContainer _FileContainer;
  93. /// <summary>
  94. /// The FileHandlerFactoryLocator
  95. /// </summary>
  96. public FileHandlerFactoryLocator FileHandlerFactoryLocator
  97. {
  98. get { return _FileHandlerFactoryLocator; }
  99. set { _FileHandlerFactoryLocator = value; }
  100. }
  101. private FileHandlerFactoryLocator _FileHandlerFactoryLocator;
  102. /// <summary>
  103. /// The default action to run if no action or method is specified in the URL
  104. /// </summary>
  105. public virtual string ImplicitAction
  106. {
  107. get { return "View"; }
  108. }
  109. /// <summary>
  110. /// The Javascript wrappers for this object. Set by GetJavascriptWrapper. This can accumulate in memory because the WebHandlers are cached and collected as they fall out of use
  111. /// </summary>
  112. Dictionary<WrapperCallsThrough, string> JavascriptWrappers = new Dictionary<WrapperCallsThrough, string>();
  113. /// <summary>
  114. /// The cached in-browser JavaScript wrapper
  115. /// </summary>
  116. private string cachedInBrowserJSWrapper = null;
  117. /// <summary>
  118. /// Returns a Javascript object that can perform all calls to all methods marked as WebCallable through AJAX.
  119. /// </summary>
  120. /// <param name="webConnection"></param>
  121. /// <param name="assignToVariable">The variable to assign the wrapper object to</param>
  122. /// <param name="EncodeFor">If set to "JavaScript", the generated JavaScript will be minimized</param>
  123. /// <returns></returns>
  124. [WebCallable(WebCallingConvention.GET_application_x_www_form_urlencoded, WebReturnConvention.JavaScriptObject, FilePermissionEnum.Read)]
  125. public IWebResults GetJSW(IWebConnection webConnection, string assignToVariable, string EncodeFor)
  126. {
  127. // Not worth syncronizing, nothing bad will happen if multiple threads enter this block at the same time
  128. if (null == cachedInBrowserJSWrapper)
  129. {
  130. string javascriptWrapper = StringGenerator.GenerateSeperatedList(
  131. FileHandlerFactoryLocator.WebServer.JavascriptWebAccessCodeGenerator.GenerateWrapper(GetType()), ",\n");
  132. // Replace some key constants
  133. javascriptWrapper = javascriptWrapper.Replace("{0}", FileContainer.FullPath);
  134. javascriptWrapper = javascriptWrapper.Replace("{1}", FileContainer.Filename);
  135. cachedInBrowserJSWrapper = javascriptWrapper.Replace("{2}", "http://" + FileHandlerFactoryLocator.HostnameAndPort + FileContainer.FullPath);
  136. }
  137. string javascriptToReturn = cachedInBrowserJSWrapper;
  138. // Insert the user's permission to the file
  139. javascriptToReturn = javascriptToReturn.Replace("{3}", FileContainer.LoadPermission(webConnection.Session.User.Id).ToString());
  140. // Insert the server-side Javascript wrappers
  141. try
  142. {
  143. IExecutionEnvironment executionEnvironment = GetOrCreateExecutionEnvironment();
  144. if (null != executionEnvironment)
  145. {
  146. string serversideJavscriptWrapper = StringGenerator.GenerateCommaSeperatedList(
  147. executionEnvironment.GenerateJavascriptWrapper(webConnection));
  148. serversideJavscriptWrapper = serversideJavscriptWrapper.Replace("{0}", FileContainer.FullPath);
  149. javascriptToReturn = javascriptToReturn + ",\n" + serversideJavscriptWrapper;
  150. }
  151. }
  152. catch (Exception e)
  153. {
  154. log.ErrorFormat("Exception occured when trying to generate a Javascript wrapper for server-side Javascript", e);
  155. }
  156. // Enclose the functions with { .... }
  157. javascriptToReturn = "{\n" + javascriptToReturn + "\n}";
  158. if (null != assignToVariable)
  159. javascriptToReturn = string.Format("var {0} = {1};", assignToVariable, javascriptToReturn);
  160. javascriptToReturn = "// Scripts: /API/AJAX.js, /API/json2.js\n" + javascriptToReturn;
  161. if (EncodeFor == "JavaScript")
  162. if (FileHandlerFactoryLocator.WebServer.MinimizeJavascript)
  163. {
  164. // The text will be "minimized" javascript to save space
  165. JavaScriptMinifier javaScriptMinifier = new JavaScriptMinifier();
  166. try
  167. {
  168. javascriptToReturn = javaScriptMinifier.Minify(javascriptToReturn);
  169. }
  170. catch (Exception e)
  171. {
  172. log.Error("Error when minimizing JavaScript", e);
  173. return WebResults.FromString(Status._500_Internal_Server_Error, "Error when minimizing JavaScript: " + e.Message);
  174. }
  175. }
  176. IWebResults toReturn = WebResults.FromString(
  177. Status._200_OK,
  178. javascriptToReturn);
  179. toReturn.ContentType = "application/javascript";
  180. return toReturn;
  181. }
  182. /// <summary>
  183. /// This should return a Javascript object that can perform all calls to all methods marked as WebCallable through server-side Javascript.
  184. /// </summary>
  185. /// <param name="webConnection"></param>
  186. /// <param name="assignToVariable">The variable to assign the wrapper object to</param>
  187. /// <returns></returns>
  188. [WebCallable(WebCallingConvention.GET_application_x_www_form_urlencoded, WebReturnConvention.JavaScriptObject, FilePermissionEnum.Read)]
  189. public IWebResults GetServersideJavascriptWrapper(IWebConnection webConnection, string assignToVariable)
  190. {
  191. string javascriptToReturn = GetJavascriptWrapper(webConnection, assignToVariable, WrapperCallsThrough.ServerSideShells);
  192. IWebResults toReturn = WebResults.FromString(Status._200_OK, javascriptToReturn);
  193. toReturn.ContentType = "application/javascript";
  194. return toReturn;
  195. }
  196. /// <summary>
  197. /// Used internally
  198. /// </summary>
  199. /// <param name="webConnection"></param>
  200. /// <param name="assignToVariable"></param>
  201. /// <returns></returns>
  202. public string GetJavascriptWrapperForBase(IWebConnection webConnection, string assignToVariable)
  203. {
  204. return GetJavascriptWrapper(webConnection, assignToVariable, WrapperCallsThrough.ServerSideShells | WrapperCallsThrough.BypassServerSideJavascript);
  205. }
  206. /// <summary>
  207. /// This should return a Javascript object that can perform all calls to all methods marked as WebCallable through.
  208. /// </summary>
  209. /// <param name="webConnection"></param>
  210. /// <param name="assignToVariable">The variable to assign the wrapper object to</param>
  211. /// <param name="wrapperCallsThrough">Indicates either to generate server-side Javascript or AJAX calls</param>
  212. /// <returns></returns>
  213. private string GetJavascriptWrapper(IWebConnection webConnection, string assignToVariable, WrapperCallsThrough wrapperCallsThrough)
  214. {
  215. if (!JavascriptWrappers.ContainsKey(wrapperCallsThrough))
  216. {
  217. string javascriptWrapper = StringGenerator.GenerateSeperatedList(
  218. FileHandlerFactoryLocator.WebServer.JavascriptWebAccessCodeGenerator.GenerateLegacyWrapper(GetType(), wrapperCallsThrough), ",\n");
  219. // Replace some key constants
  220. javascriptWrapper = javascriptWrapper.Replace("{0}", FileContainer.FullPath);
  221. javascriptWrapper = javascriptWrapper.Replace("{1}", FileContainer.Filename);
  222. JavascriptWrappers[wrapperCallsThrough] = javascriptWrapper.Replace("{2}", "http://" + FileHandlerFactoryLocator.HostnameAndPort + FileContainer.FullPath);
  223. }
  224. string javascriptToReturn = JavascriptWrappers[wrapperCallsThrough];
  225. // Insert the user's permission to the file
  226. javascriptToReturn = javascriptToReturn.Replace("{3}", FileContainer.LoadPermission(webConnection.Session.User.Id).ToString());
  227. if ((WrapperCallsThrough.BypassServerSideJavascript & wrapperCallsThrough) == 0)
  228. try
  229. {
  230. IExecutionEnvironment executionEnvironment = GetOrCreateExecutionEnvironment();
  231. if (null != executionEnvironment)
  232. {
  233. string serversideJavscriptWrapper = StringGenerator.GenerateCommaSeperatedList(
  234. executionEnvironment.GenerateLegacyJavascriptWrapper(webConnection, wrapperCallsThrough));
  235. serversideJavscriptWrapper = serversideJavscriptWrapper.Replace("{0}", FileContainer.FullPath);
  236. javascriptToReturn = javascriptToReturn + ",\n" + serversideJavscriptWrapper;
  237. }
  238. }
  239. catch (Exception e)
  240. {
  241. log.ErrorFormat("Exception occured when trying to generate a Javascript wrapper for server-side Javascript", e);
  242. }
  243. // Enclose the functions with { .... }
  244. javascriptToReturn = "{\n" + javascriptToReturn + "\n}";
  245. if (null != assignToVariable)
  246. javascriptToReturn = string.Format("var {0} = {1};", assignToVariable, javascriptToReturn);
  247. return javascriptToReturn;
  248. }
  249. /// <summary>
  250. /// Returns any syntax errors that occur as a result of trying to load server-side javascript
  251. /// </summary>
  252. /// <param name="webConnection"></param>
  253. /// <returns></returns>
  254. [WebCallable(WebCallingConvention.GET, WebReturnConvention.Primitive, FilePermissionEnum.Administer)]
  255. public IWebResults GetServersideJavascriptErrors(IWebConnection webConnection)
  256. {
  257. IExecutionEnvironment executionEnvironment = GetOrCreateExecutionEnvironment();
  258. return WebResults.FromString(Status._200_OK, executionEnvironment.ExecutionEnvironmentErrors != null ? executionEnvironment.ExecutionEnvironmentErrors : "no errors");
  259. }
  260. /// <summary>
  261. /// Sets the user's permission for the given file. Either the user or group ID or name are set
  262. /// </summary>
  263. /// <param name="webConnection"></param>
  264. /// <returns></returns>
  265. /// <param name="FilePermission">The permission, set to null to disable permissions to the file</param>
  266. /// <param name="Inherit">Set to true to allow permission inheritance. For example, if this permission applies to a directory, it will be the default for files in the directory</param>
  267. /// <param name="UserOrGroup"></param>
  268. /// <param name="UserOrGroupId"></param>
  269. /// <param name="SendNotifications"></param>
  270. [WebCallable(WebCallingConvention.POST_application_x_www_form_urlencoded, WebReturnConvention.Primitive, FilePermissionEnum.Administer)]
  271. public IWebResults SetPermission(IWebConnection webConnection, string UserOrGroupId, string UserOrGroup, string FilePermission, bool? Inherit, bool? SendNotifications)
  272. {
  273. ID<IUserOrGroup, Guid> userOrGroupId;
  274. if (null != UserOrGroupId)
  275. userOrGroupId = new ID<IUserOrGroup, Guid>(new Guid(UserOrGroupId));
  276. else
  277. try
  278. {
  279. userOrGroupId = FileHandlerFactoryLocator.UserManagerHandler.GetUserOrGroupOrOpenId(UserOrGroup).Id;
  280. }
  281. catch (UnknownUser)
  282. {
  283. return WebResults.FromString(Status._406_Not_Acceptable, UserOrGroup + " does not exist");
  284. }
  285. FilePermissionEnum? level = null;
  286. if (null != FilePermission)
  287. level = Enum<FilePermissionEnum>.TryParse(FilePermission);
  288. bool inherit = false;
  289. if (null != Inherit)
  290. inherit = Inherit.Value;
  291. bool sendNotifications = false;
  292. if (null != SendNotifications)
  293. sendNotifications = SendNotifications.Value;
  294. // Null permissions just remove the permission
  295. if (null != level)
  296. {
  297. FileHandler.FileContainer.ParentDirectoryHandler.SetPermission(webConnection.Session.User.Id, FileHandler.FileContainer.Filename, userOrGroupId, level.Value, inherit, sendNotifications);
  298. return WebResults.FromString(Status._202_Accepted, "Permission set to " + level.ToString());
  299. }
  300. else
  301. {
  302. FileHandler.FileContainer.ParentDirectoryHandler.RemovePermission(FileHandler.FileContainer.Filename, userOrGroupId);
  303. return WebResults.FromString(Status._202_Accepted, "Permission removed");
  304. }
  305. }
  306. /// <summary>
  307. /// Returns the currently logged un user's permission for this file. If the user doesn't have an assigned permission, a 0-length string is returned.
  308. /// </summary>
  309. /// <param name="webConnection"></param>
  310. /// <returns></returns>
  311. [WebCallable(WebCallingConvention.GET, WebReturnConvention.Primitive)]
  312. public IWebResults GetPermission(IWebConnection webConnection)
  313. {
  314. FilePermissionEnum? permission = FileContainer.LoadPermission(webConnection.Session.User.Id);
  315. if (null != permission)
  316. return WebResults.FromString(Status._200_OK, permission.ToString());
  317. else
  318. return WebResults.FromString(Status._200_OK, "");
  319. }
  320. /// <summary>
  321. /// Returns the currently logged in user's permission for this file as a Javascript object that can be queried.
  322. /// </summary>
  323. /// <param name="webConnection"></param>
  324. /// <returns></returns>
  325. [WebCallable(WebCallingConvention.GET, WebReturnConvention.JSON)]
  326. public IWebResults GetPermissionAsJSON(IWebConnection webConnection)
  327. {
  328. // Create an array of values to return
  329. Dictionary<string, object> toReturn = new Dictionary<string, object>();
  330. FilePermissionEnum? permissionNullable = FileContainer.LoadPermission(webConnection.Session.User.Id);
  331. if (null != permissionNullable)
  332. {
  333. FilePermissionEnum permission = permissionNullable.Value;
  334. foreach (FilePermissionEnum fpe in Enum<FilePermissionEnum>.Values)
  335. toReturn["Can" + fpe.ToString()] = permission >= fpe;
  336. toReturn["Permission"] = permission.ToString();
  337. }
  338. else
  339. foreach (FilePermissionEnum fpe in Enum<FilePermissionEnum>.Values)
  340. toReturn["Can" + fpe.ToString()] = false;
  341. return WebResults.ToJson(toReturn);
  342. }
  343. /// <summary>
  344. /// Returns all assigned permissions to the object
  345. /// </summary>
  346. /// <param name="webConnection">
  347. /// A <see cref="IWebConnection"/>
  348. /// </param>
  349. /// <returns>
  350. /// A <see cref="IWebResults"/>
  351. /// </returns>
  352. [WebCallable(WebCallingConvention.GET, WebReturnConvention.JSON, FilePermissionEnum.Administer)]
  353. public IWebResults GetPermissions(IWebConnection webConnection)
  354. {
  355. IUserManagerHandler userManagerHandler = FileHandlerFactoryLocator.UserManagerHandler;
  356. List<object> permissionsList = new List<object>();
  357. Dictionary<ID<IUserOrGroup, Guid>, FilePermission> permissionsById = new Dictionary<ID<IUserOrGroup, Guid>, FilePermission>();
  358. foreach (FilePermission filePermission in FileHandler.FileContainer.ParentDirectoryHandler.GetPermissions(FileHandler.FileContainer.Filename))
  359. permissionsById[filePermission.UserOrGroupId] = filePermission;
  360. foreach (IUserOrGroup userOrGroup in userManagerHandler.GetUsersAndGroups(permissionsById.Keys))
  361. {
  362. Dictionary<string, object> permission = new Dictionary<string, object>();
  363. FilePermission filePermission = permissionsById[userOrGroup.Id];
  364. permission["Id"] = userOrGroup.Id.Value;
  365. permission["Permission"] = filePermission.FilePermissionEnum;
  366. permission["Name"] = userOrGroup.Name;
  367. permission["Inherit"] = filePermission.Inherit;
  368. permission["SendNotifications"] = filePermission.SendNotifications;
  369. permissionsList.Add(permission);
  370. }
  371. return WebResults.ToJson(permissionsList);
  372. }
  373. /// <summary>
  374. /// Sets a named permission
  375. /// </summary>
  376. /// <param name="webConnection"></param>
  377. /// <param name="usernameOrGroup"></param>
  378. /// <param name="namedPermission"></param>
  379. /// <param name="inherit"></param>
  380. /// <returns></returns>
  381. [WebCallable(WebCallingConvention.POST_application_x_www_form_urlencoded, WebReturnConvention.Status, FilePermissionEnum.Administer)]
  382. public IWebResults SetNamedPermission(IWebConnection webConnection, string usernameOrGroup, string namedPermission, bool inherit)
  383. {
  384. IUserOrGroup userOrGroup = FileHandlerFactoryLocator.UserManagerHandler.GetUserOrGroupOrOpenId(usernameOrGroup);
  385. FileContainer.ParentDirectoryHandler.SetNamedPermission(
  386. FileContainer.FileId,
  387. namedPermission,
  388. userOrGroup.Id,
  389. inherit);
  390. return WebResults.FromStatus(Status._202_Accepted);
  391. }
  392. /// <summary>
  393. /// Removes the named permission
  394. /// </summary>
  395. /// <param name="webConnection"></param>
  396. /// <param name="usernameOrGroup"></param>
  397. /// <param name="namedPermission"></param>
  398. /// <returns></returns>
  399. [WebCallable(WebCallingConvention.POST_application_x_www_form_urlencoded, WebReturnConvention.Status, FilePermissionEnum.Administer)]
  400. public IWebResults RemoveNamedPermission(IWebConnection webConnection, string usernameOrGroup, string namedPermission)
  401. {
  402. IUserOrGroup userOrGroup = FileHandlerFactoryLocator.UserManagerHandler.GetUserOrGroupOrOpenId(usernameOrGroup);
  403. FileContainer.ParentDirectoryHandler.RemoveNamedPermission(
  404. FileContainer.FileId,
  405. namedPermission,
  406. userOrGroup.Id);
  407. return WebResults.FromStatus(Status._202_Accepted);
  408. }
  409. /// <summary>
  410. /// Returns all of the users and groups that have the named permission
  411. /// </summary>
  412. /// <param name="webConnection"></param>
  413. /// <param name="namedPermission"></param>
  414. /// <returns></returns>
  415. [WebCallable(WebCallingConvention.GET_application_x_www_form_urlencoded, WebReturnConvention.JavaScriptObject, FilePermissionEnum.Administer)]
  416. public IWebResults GetNamedPermissions(IWebConnection webConnection, string namedPermission)
  417. {
  418. List<object> toReturn = new List<object>();
  419. foreach (NamedPermission np in FileContainer.ParentDirectoryHandler.GetNamedPermissions(
  420. FileContainer.FileId, namedPermission))
  421. {
  422. Dictionary<string, object> toJSON = new Dictionary<string, object>();
  423. toJSON["UserOrGroupId"] = np.UserOrGroupId;
  424. toJSON["UserOrGroup"] = FileHandlerFactoryLocator.UserManagerHandler.GetUserOrGroup(np.UserOrGroupId).Name;
  425. toJSON["Inherit"] = np.Inherit;
  426. toReturn.Add(toJSON);
  427. }
  428. return WebResults.ToJson(toReturn);
  429. }
  430. /// <summary>
  431. /// Returns true if the user has the named permission, false otherwise
  432. /// </summary>
  433. /// <param name="webConnection"></param>
  434. /// <param name="namedPermission"></param>
  435. /// <returns></returns>
  436. [WebCallable(WebCallingConvention.GET_application_x_www_form_urlencoded, WebReturnConvention.JavaScriptObject, FilePermissionEnum.Read)]
  437. public IWebResults HasNamedPermission(IWebConnection webConnection, string namedPermission)
  438. {
  439. if (null == FileContainer.ParentDirectoryHandler)
  440. throw new WebResultsOverrideException(WebResults.FromString(Status._400_Bad_Request, "Permissions do not apply to the root directory"));
  441. bool hasPermission = FileContainer.ParentDirectoryHandler.HasNamedPermissions(
  442. FileContainer.FileId, new string[] { namedPermission }, webConnection.Session.User.Id);
  443. return WebResults.ToJson(hasPermission);
  444. }
  445. /// <summary>
  446. /// Performs any needed cleanup and optimization operations needed on the file
  447. /// </summary>
  448. /// <param name="webConnection">
  449. /// A <see cref="IWebConnection"/>
  450. /// </param>
  451. /// <returns>
  452. /// A <see cref="IWebResults"/>
  453. /// </returns>
  454. [WebCallable(WebCallingConvention.POST_application_x_www_form_urlencoded, WebReturnConvention.Primitive, FilePermissionEnum.Administer)]
  455. public IWebResults Vacuum(IWebConnection webConnection)
  456. {
  457. FileHandler.Vacuum();
  458. return WebResults.FromStatus(Status._200_OK);
  459. }
  460. #region Common bus methods
  461. /// <summary>
  462. /// The object's bus. Messages can be written to the bus without having to open a Comet session; any user with read permission to the object can open a comet session and see all messages on the bus
  463. /// </summary>
  464. [ChannelEndpointMinimumPermission(FilePermissionEnum.Read)]
  465. public IChannelEventWebAdaptor Bus
  466. {
  467. get
  468. {
  469. if (null == _Bus)
  470. {
  471. _Bus = new ChannelEventWebAdaptor();
  472. _Bus.ClientConnected += new EventHandler<IChannelEventWebAdaptor, EventArgs<IQueuingReliableCometTransport>>(Bus_ClientConnected);
  473. _Bus.ClientDisconnected += new EventHandler<IChannelEventWebAdaptor, EventArgs<IQueuingReliableCometTransport>>(Bus_ClientDisconnected);
  474. _Bus.DataReceived += new EventHandler<IChannelEventWebAdaptor, ChannelEventWebAdaptor.DataReceivedEventArgs>(Bus_DataReceived);
  475. }
  476. return _Bus;
  477. }
  478. }
  479. private ChannelEventWebAdaptor _Bus = null;
  480. /// <summary>
  481. /// Helper to create a JSON object for a user
  482. /// </summary>
  483. /// <param name="user"></param>
  484. /// <returns></returns>
  485. private Dictionary<string, object> CreateUserObjectForJSON(IUser user)
  486. {
  487. Dictionary<string, object> toReturn = new Dictionary<string, object>();
  488. toReturn["User"] = user.Name;
  489. toReturn["UserId"] = user.Id.ToString();
  490. toReturn["UserIdentity"] = user.Identity;
  491. return toReturn;
  492. }
  493. void Bus_DataReceived(IChannelEventWebAdaptor sender, ChannelEventWebAdaptor.DataReceivedEventArgs e)
  494. {
  495. PostBus(e.User, e.Data, "Bus");
  496. }
  497. void Bus_ClientDisconnected(IChannelEventWebAdaptor sender, EventArgs<IQueuingReliableCometTransport> e)
  498. {
  499. Dictionary<string, object> toSend = CreateUserObjectForJSON(e.Value.Session.User);
  500. toSend["Disconnected"] = true;
  501. toSend["Timestamp"] = DateTime.UtcNow;
  502. Bus.SendAll(toSend);
  503. }
  504. void Bus_ClientConnected(IChannelEventWebAdaptor sender, EventArgs<IQueuingReliableCometTransport> e)
  505. {
  506. Dictionary<string, object> toSend = CreateUserObjectForJSON(e.Value.Session.User);
  507. toSend["Connected"] = true;
  508. toSend["Timestamp"] = DateTime.UtcNow;
  509. Bus.SendAll(toSend);
  510. }
  511. /// <summary>
  512. /// Posts a message to the bus as coming from someone with read permission
  513. /// </summary>
  514. /// <param name="webConnection"></param>
  515. /// <param name="incoming">The message to post to the bus</param>
  516. /// <returns></returns>
  517. [WebCallable(WebCallingConvention.POST_string, WebReturnConvention.Status, FilePermissionEnum.Read)]
  518. public IWebResults PostBusAsRead(IWebConnection webConnection, string incoming)
  519. {
  520. object fromClient = JsonReader.Deserialize(incoming);
  521. PostBus(webConnection.Session.User, fromClient, "Read");
  522. return WebResults.FromStatus(Status._202_Accepted);
  523. }
  524. /// <summary>
  525. /// Posts a message to the bus as coming from someone with write permission
  526. /// </summary>
  527. /// <param name="webConnection"></param>
  528. /// <param name="incoming">The message to post to the bus</param>
  529. /// <returns></returns>
  530. [WebCallable(WebCallingConvention.POST_string, WebReturnConvention.Status, FilePermissionEnum.Write)]
  531. public IWebResults PostBusAsWrite(IWebConnection webConnection, string incoming)
  532. {
  533. object fromClient = JsonReader.Deserialize(incoming);
  534. PostBus(webConnection.Session.User, fromClient, "Write");
  535. return WebResults.FromStatus(Status._202_Accepted);
  536. }
  537. /// <summary>
  538. /// Posts a message to the bus as coming from someone with administer permission
  539. /// </summary>
  540. /// <param name="webConnection"></param>
  541. /// <param name="incoming"></param>
  542. /// <returns></returns>
  543. [WebCallable(WebCallingConvention.POST_string, WebReturnConvention.Status, FilePermissionEnum.Administer)]
  544. public IWebResults PostBusAsAdminister(IWebConnection webConnection, string incoming)
  545. {
  546. object fromClient = JsonReader.Deserialize(incoming);
  547. PostBus(webConnection.Session.User, fromClient, "Administer");
  548. return WebResults.FromStatus(Status._202_Accepted);
  549. }
  550. /// <summary>
  551. /// Posts some data to the bus
  552. /// </summary>
  553. /// <param name="data"></param>
  554. /// <param name="source"></param>
  555. /// <param name="user"></param>
  556. /// <returns></returns>
  557. private void PostBus(IUser user, object data, string source)
  558. {
  559. Dictionary<string, object> toSend = CreateUserObjectForJSON(user);
  560. toSend["Source"] = source;
  561. toSend["Data"] = data;
  562. toSend["Timestamp"] = DateTime.UtcNow;
  563. Bus.SendAll(toSend);
  564. }
  565. /// <summary>
  566. /// Returns all of the users connected to the object
  567. /// </summary>
  568. /// <param name="webConnection"></param>
  569. /// <returns></returns>
  570. [WebCallable(WebCallingConvention.GET, WebReturnConvention.JavaScriptObject)]
  571. public IWebResults GetConnectedUsers(IWebConnection webConnection)
  572. {
  573. Dictionary<IUser, Dictionary<string, object>> usersByUser = new Dictionary<IUser, Dictionary<string, object>>();
  574. foreach (ISession session in Bus.ConnectedSessions)
  575. {
  576. Dictionary<string, object> userToSend = null;
  577. if (usersByUser.TryGetValue(session.User, out userToSend))
  578. userToSend["NumWindows"] = ((int)userToSend["NumWindows"]) + 1;
  579. else
  580. {
  581. userToSend = CreateUserObjectForJSON(session.User);
  582. userToSend["NumWindows"] = 1;
  583. usersByUser[session.User] = userToSend;
  584. }
  585. }
  586. return WebResults.ToJson(usersByUser.Values);
  587. }
  588. #endregion
  589. #region Comet handlers
  590. /// <summary>
  591. /// Called as part of the loop of a transport (level 1) comet session
  592. /// </summary>
  593. /// <param name="webConnection"></param>
  594. /// <returns></returns>
  595. [WebCallable(WebCallingConvention.Naked, WebReturnConvention.JavaScriptObject)]
  596. public IWebResults PostComet(IWebConnection webConnection)
  597. {
  598. Dictionary<string, object> fromClient = JsonReader.Deserialize<Dictionary<string, object>>(webConnection.Content.AsString());
  599. object transportIdObject = null;
  600. if (!fromClient.TryGetValue("tid", out transportIdObject))
  601. return WebResults.FromString(Status._400_Bad_Request, "Transport id (tid) missing.");
  602. long transportId = default(long);
  603. try
  604. {
  605. transportId = Convert.ToInt64(transportIdObject);
  606. }
  607. catch
  608. {
  609. log.Error("Invalid transport ID: " + transportIdObject.ToString());
  610. return WebResults.FromString(Status._400_Bad_Request, "Transport id is invalid. It must be an integer");
  611. }
  612. object timeoutObject = null;
  613. if (!fromClient.TryGetValue("lp", out timeoutObject))
  614. return WebResults.FromString(Status._400_Bad_Request, "Long poll (lp) missing.");
  615. double timeoutDouble = default(double);
  616. try
  617. {
  618. timeoutDouble = Convert.ToDouble(timeoutObject);
  619. }
  620. catch
  621. {
  622. log.Error("Invalid long poll: " + transportIdObject.ToString());
  623. return WebResults.FromString(Status._400_Bad_Request, "Long-poll is invalid. It must be an number");
  624. }
  625. TimeSpan longPoll = TimeSpan.FromMilliseconds(timeoutDouble);
  626. ICometTransport cometTransport;
  627. if (fromClient.ContainsKey("isNew"))
  628. cometTransport = CreateNewCometTransport(webConnection.Session, webConnection.GetParameters, transportId);
  629. else
  630. cometTransport = GetCometTransport(webConnection.Session, transportId);
  631. if (fromClient.ContainsKey("d"))
  632. cometTransport.HandleIncomingData(fromClient["d"]);
  633. // If there's data to send, return it NOW
  634. if (PendingCometListeners.ContainsKey(cometTransport))
  635. {
  636. // This will cause the old long poll to send current pending results
  637. PendingCometListeners[cometTransport].TimeoutHandler(cometTransport);
  638. PendingCometListeners.Remove(cometTransport);
  639. }
  640. else
  641. {
  642. object data = cometTransport.GetDataToSend();
  643. if (null != data)
  644. return WebResults.ToJson(data);
  645. }
  646. // if long-poll is disabled, return NOW without a long-poll
  647. if (0 == longPoll.TotalMilliseconds)
  648. webConnection.SendResults(WebResults.FromStatus(Status._200_OK));
  649. // If any pending data was returned on an old long poll, or there's no pending data, go to long-poll mode and wait for data
  650. MulticastEventWithTimeout<ICometTransport, EventArgs<TimeSpan>>.Listener listener =
  651. default(MulticastEventWithTimeout<ICometTransport, EventArgs<TimeSpan>>.Listener);
  652. listener = new MulticastEventWithTimeout<ICometTransport, EventArgs<TimeSpan>>.Listener(
  653. longPoll,
  654. delegate(ICometTransport cometTransportT, EventArgs<TimeSpan> e)
  655. {
  656. // TODO: Honor the send delay
  657. cometTransport.StartSend.RemoveListener(listener);
  658. PendingCometListeners.Remove(cometTransport);
  659. object data = cometTransport.GetDataToSend();
  660. // Only return data if there was actually data to send
  661. if (null != data)
  662. webConnection.SendResults(WebResults.ToJson(data));
  663. },
  664. delegate(ICometTransport cometTransportT)
  665. {
  666. cometTransport.StartSend.RemoveListener(listener);
  667. PendingCometListeners.Remove(cometTransport);
  668. object data = cometTransport.GetDataToSend();
  669. if (null != data)
  670. webConnection.SendResults(WebResults.ToJson(data));
  671. else
  672. webConnection.SendResults(WebResults.FromStatus(Status._200_OK));
  673. });
  674. cometTransport.StartSend.AddListener(listener);
  675. PendingCometListeners[cometTransport] = listener;
  676. return null;
  677. }
  678. /// <summary>
  679. /// Pending comet listeners
  680. /// </summary>
  681. private Dictionary<ICometTransport, MulticastEventWithTimeout<ICometTransport, EventArgs<TimeSpan>>.Listener> PendingCometListeners =
  682. new Dictionary<ICometTransport, MulticastEventWithTimeout<ICometTransport, EventArgs<TimeSpan>>.Listener>();
  683. /// <summary>
  684. /// Used to uniquely identify a comet session
  685. /// </summary>
  686. private struct CometSessionId
  687. {
  688. public CometSessionId(ID<ISession, Guid> sessionId, long transportId)
  689. {
  690. SessionId = sessionId;
  691. TransportId = transportId;
  692. _Hash = default(int?);
  693. }
  694. public ID<ISession, Guid> SessionId;
  695. public long TransportId;
  696. public override bool Equals(object obj)
  697. {
  698. if (!(obj is CometSessionId))
  699. return false;
  700. CometSessionId csId = (CometSessionId)obj;
  701. return (SessionId == csId.SessionId) && (TransportId == csId.TransportId);
  702. }
  703. static MD5 md5 = new MD5CryptoServiceProvider();
  704. public override int GetHashCode()
  705. {
  706. if (default(int?) == _Hash)
  707. {
  708. List<byte> toHash = new List<byte>();
  709. toHash.AddRange(SessionId.Value.ToByteArray());
  710. toHash.AddRange(BitConverter.GetBytes(TransportId));
  711. byte[] hash = md5.ComputeHash(toHash.ToArray());
  712. _Hash = BitConverter.ToInt32(hash, 0);
  713. }
  714. return _Hash.Value;
  715. }
  716. private int? _Hash;
  717. public override string ToString()
  718. {
  719. return SessionId.ToString() + "-" + TransportId.ToString();
  720. }
  721. }
  722. /// <summary>
  723. /// Used to track if a comet session is going idle
  724. /// </summary>
  725. private class CometSessionTracker
  726. {
  727. public ICometTransport CometTransport;
  728. public DateTime LastUsed;
  729. }
  730. /// <summary>
  731. /// All of the comet transports that are currently active
  732. /// </summary>
  733. private Dictionary<CometSessionId, CometSessionTracker> ActiveCometTransports = new Dictionary<CometSessionId, CometSessionTracker>();
  734. /// <summary>
  735. /// Creates a new comet transport as part of a transport (level 1) or multiplexed (level 2) comet session
  736. /// </summary>
  737. /// <param name="session"></param>
  738. /// <param name="transportId"></param>
  739. /// <param name="getArguments"></param>
  740. /// <returns></returns>
  741. public ICometTransport CreateNewCometTransport(ISession session, IDictionary<string, string> getArguments, long transportId)
  742. {
  743. CometSessionId id = new CometSessionId(session.SessionId, transportId);
  744. if (ActiveCometTransports.ContainsKey(id))
  745. throw new WebResultsOverrideException(WebResults.FromStatus(Status._409_Conflict));
  746. CometSessionTracker toReturn = new CometSessionTracker();
  747. toReturn.CometTransport = ConstructCometTransport(session, getArguments, transportId);
  748. using (TimedLock.Lock(ActiveCometTransports))
  749. ActiveCometTransports[id] = toReturn;
  750. toReturn.LastUsed = DateTime.UtcNow;
  751. using (TimedLock.Lock(PurgeOldCometSessionsKey))
  752. if (null == PurgeOldCometSessionsTimer)
  753. {
  754. // This no-op is to work around a weird mono compiler bug... Get rid of it once the mono compiler is updated!
  755. // https://bugzilla.novell.com/show_bug.cgi?id=554715
  756. if (noop < 0) noop = SRandom.Next<int>();
  757. PurgeOldCometSessionsTimer = new Timer(
  758. CleanOldTransports,
  759. null,
  760. TimeSpan.FromMilliseconds(0),
  761. TimeSpan.FromSeconds(FileHandlerFactoryLocator.WebServer.CheckDeadConnectionsFrequencySeconds));
  762. }
  763. return toReturn.CometTransport;
  764. }
  765. private static int noop = 0;
  766. /// <summary>
  767. /// Holds all properties and permissions by Type for channel endpoints
  768. /// </summary>
  769. private static Dictionary<Type, Dictionary<string, PropertyAndPermission>> ChannelEndpointPropertiesAndPermissionsByType =
  770. new Dictionary<Type, Dictionary<string, PropertyAndPermission>>();
  771. /// <summary>
  772. /// Holds property and permission
  773. /// </summary>
  774. private struct PropertyAndPermission
  775. {
  776. internal PropertyInfo Property;
  777. internal FilePermissionEnum MinimumPermission;
  778. }
  779. /// <summary>
  780. /// Constructs a comet transport for transport (level 1) or multiplexed (level 2) comet
  781. /// </summary>
  782. /// <param name="session"></param>
  783. /// <param name="transportId"></param>
  784. /// <param name="getArguments"></param>
  785. /// <returns></returns>
  786. public virtual ICometTransport ConstructCometTransport(ISession session, IDictionary<string, string> getArguments, long transportId)
  787. {
  788. string channelEndpointName = null;
  789. if (getArguments.TryGetValue("ChannelEndpoint", out channelEndpointName))
  790. {
  791. Type myType = GetType();
  792. // Reflect on the class if needed to figure out what channel endpoints are present
  793. if (!ChannelEndpointPropertiesAndPermissionsByType.ContainsKey(myType))
  794. using (TimedLock.Lock(ChannelEndpointPropertiesAndPermissionsByType))
  795. if (!ChannelEndpointPropertiesAndPermissionsByType.ContainsKey(myType))
  796. {
  797. Dictionary<string, PropertyAndPermission> channelEndpointPropertiesAndPermissions = new Dictionary<string, PropertyAndPermission>();
  798. foreach (PropertyInfo pi in myType.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public))
  799. if (typeof(IChannelEventWebAdaptor) == pi.PropertyType)
  800. foreach (ChannelEndpointMinimumPermissionAttribute cempa in
  801. pi.GetCustomAttributes(typeof(ChannelEndpointMinimumPermissionAttribute), true))
  802. {
  803. PropertyAndPermission pap = new PropertyAndPermission();
  804. pap.Property = pi;
  805. pap.MinimumPermission = cempa.MinimumPermission;
  806. channelEndpointPropertiesAndPermissions[pi.Name] = pap;
  807. }
  808. ChannelEndpointPropertiesAndPermissionsByType[myType] = channelEndpointPropertiesAndPermissions;
  809. }
  810. // Load and return the channel, if present
  811. PropertyAndPermission propertyAndPermission = default(PropertyAndPermission);
  812. if (ChannelEndpointPropertiesAndPermissionsByType[myType].TryGetValue(channelEndpointName, out propertyAndPermission))
  813. {
  814. if (FileContainer.LoadPermission(session.User.Id) < propertyAndPermission.MinimumPermission)
  815. throw new WebResultsOverrideException(WebResults.FromString(Status._401_Unauthorized, "Permission denied"));
  816. QueuingReliableCometTransport toReturn =
  817. new QueuingReliableCometTransport(FileContainer.FullPath + "?ChannelEndpoint=" + channelEndpointName, session);
  818. IChannelEventWebAdaptor channelEventWebAdaptor = (IChannelEventWebAdaptor)propertyAndPermission.Property.GetValue(this, null);
  819. channelEventWebAdaptor.AddChannel(toReturn);
  820. return toReturn;
  821. }
  822. }
  823. throw new WebResultsOverrideException(WebResults.FromStatus(Status._404_Not_Found));
  824. }
  825. /// <summary>
  826. /// Syncronizes access to PurgeOldCometSessionsTimer
  827. /// </summary>
  828. private object PurgeOldCometSessionsKey = new object();
  829. /// <summary>
  830. /// Purges dead comet sessions
  831. /// </summary>
  832. private Timer PurgeOldCometSessionsTimer = null;
  833. /// <summary>
  834. /// Retrieves a pre-exising comet transport as part of a transport (level 1) or multiplexed (level 2) comet session
  835. /// </summary>
  836. /// <param name="session"></param>
  837. /// <param name="transportId"></param>
  838. /// <returns></returns>
  839. public ICometTransport GetCometTransport(ISession session, long transportId)
  840. {
  841. CometSessionId id = new CometSessionId(session.SessionId, transportId);
  842. CometSessionTracker toReturn = default(CometSessionTracker);
  843. if (!ActiveCometTransports.TryGetValue(id, out toReturn))
  844. throw new WebResultsOverrideException(WebResults.FromStatus(Status._410_Gone));
  845. toReturn.LastUsed = DateTime.UtcNow;
  846. Console.Write("");
  847. return toReturn.CometTransport;
  848. }
  849. /// <summary>
  850. /// Cleans up old and dead transports
  851. /// </summary>
  852. private void CleanOldTransports(object state)
  853. {
  854. List<KeyValuePair<CometSessionId, CometSessionTracker>> activeCometTransports;
  855. using (TimedLock.Lock(ActiveCometTransports))
  856. activeCometTransports = new List<KeyValuePair<CometSessionId, CometSessionTracker>>(ActiveCometTransports);
  857. foreach (KeyValuePair<CometSessionId, CometSessionTracker> cst in activeCometTransports)
  858. {
  859. DateTime lastUsed = cst.Value.LastUsed;
  860. DateTime expiresAt = lastUsed.AddSeconds(FileHandlerFactoryLocator.WebServer.MaxConnectionIdleSeconds);
  861. if (expiresAt < DateTime.UtcNow)
  862. {
  863. using (TimedLock.Lock(ActiveCometTransports))
  864. ActiveCometTransports.Remove(cst.Key);
  865. cst.Value.CometTransport.Dispose();
  866. }
  867. }
  868. bool killTimer;
  869. using (TimedLock.Lock(ActiveCometTransports))
  870. killTimer = (0 == ActiveCometTransports.Count);
  871. if (killTimer)
  872. using (TimedLock.Lock(PurgeOldCometSessionsKey))
  873. if (null != PurgeOldCometSessionsTimer)
  874. {
  875. PurgeOldCometSessionsTimer.Dispose();
  876. PurgeOldCometSessionsTimer = null;
  877. }
  878. }
  879. #endregion
  880. /// <summary>
  881. /// Adds a file as being related to this file
  882. /// </summary>
  883. /// <param name="webConnection"></param>
  884. /// <param name="filename"></param>
  885. /// <param name="relationship"></param>
  886. /// <returns></returns>
  887. [WebCallable(WebCallingConvention.POST_application_x_www_form_urlencoded, WebReturnConvention.Status, FilePermissionEnum.Administer)]
  888. public IWebResults AddRelatedFile(IWebConnection webConnection, string filename, string relationship)
  889. {
  890. if (null == FileContainer.ParentDirectoryHandler)
  891. throw new WebResultsOverrideException(WebResults.FromString(Status._406_Not_Acceptable, "The root directory can not have relationships"));
  892. // Get the full path if it's not present
  893. if (!filename.StartsWith("/"))
  894. filename = FileContainer.ParentDirectoryHandler.FileContainer.FullPath + "/" + filename;
  895. IFileContainer relatedContainer;
  896. try
  897. {
  898. relatedContainer = FileHandlerFactoryLocator.FileSystemResolver.ResolveFile(filename);
  899. }
  900. catch (FileDoesNotExist)
  901. {
  902. throw new WebResultsOverrideException(WebResults.FromString(Status._404_Not_Found, filename +

Large files files are truncated, but you can click here to view the full file