Changeset 434 for binary-improvements2/WebServer/src/WebAPI
- Timestamp:
- May 17, 2023, 11:05:59 PM (20 months ago)
- Location:
- binary-improvements2/WebServer/src/WebAPI
- Files:
-
- 8 added
- 10 edited
Legend:
- Unmodified
- Added
- Removed
-
binary-improvements2/WebServer/src/WebAPI/APIs/Command.cs
r426 r434 91 91 92 92 protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) { 93 if (! TryGetJsonField (_jsonInput, "command", out string commandString)) {94 SendE rrorResult(_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_COMMAND");93 if (!JsonCommons.TryGetJsonField (_jsonInput, "command", out string commandString)) { 94 SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_COMMAND"); 95 95 return; 96 96 } … … 98 98 WebCommandResult.ResultType responseType = WebCommandResult.ResultType.Full; 99 99 100 if ( TryGetJsonField (_jsonInput, "format", out string formatString)) {100 if (JsonCommons.TryGetJsonField (_jsonInput, "format", out string formatString)) { 101 101 if (formatString.EqualsCaseInsensitive ("raw")) { 102 102 responseType = WebCommandResult.ResultType.Raw; … … 115 115 116 116 if (command == null) { 117 SendE rrorResult(_context, HttpStatusCode.NotFound, _jsonInputData, "UNKNOWN_COMMAND");117 SendEmptyResponse (_context, HttpStatusCode.NotFound, _jsonInputData, "UNKNOWN_COMMAND"); 118 118 return; 119 119 } … … 122 122 123 123 if (_context.PermissionLevel > commandPermissionLevel) { 124 SendE rrorResult(_context, HttpStatusCode.Forbidden, _jsonInputData, "NO_PERMISSION");124 SendEmptyResponse (_context, HttpStatusCode.Forbidden, _jsonInputData, "NO_PERMISSION"); 125 125 return; 126 126 } -
binary-improvements2/WebServer/src/WebAPI/APIs/GameData/Item.cs
r433 r434 3 3 using Webserver.Permissions; 4 4 5 namespace Webserver.WebAPI.APIs {5 namespace Webserver.WebAPI.APIs.GameData { 6 6 [UsedImplicitly] 7 7 internal class Item : AbsRestApi { -
binary-improvements2/WebServer/src/WebAPI/APIs/GameData/Mods.cs
r433 r434 3 3 using Webserver.Permissions; 4 4 5 namespace Webserver.WebAPI.APIs {5 namespace Webserver.WebAPI.APIs.GameData { 6 6 [UsedImplicitly] 7 7 public class Mods : AbsRestApi { … … 12 12 writer.WriteBeginArray (); 13 13 14 for (int i = 0; i < _parent. webMods.Count; i++) {15 WebMod webMod = _parent. webMods [i];14 for (int i = 0; i < _parent.WebMods.Count; i++) { 15 WebMod webMod = _parent.WebMods [i]; 16 16 17 17 if (i > 0) { … … 62 62 _writer.WriteValueSeparator (); 63 63 _writer.WritePropertyName ("displayName"); 64 JsonCommons.WriteStringOrNull (ref _writer,_webMod.ParentMod.DisplayName);64 _writer.WriteString (_webMod.ParentMod.DisplayName); 65 65 66 66 _writer.WriteValueSeparator (); 67 67 _writer.WritePropertyName ("description"); 68 JsonCommons.WriteStringOrNull (ref _writer,_webMod.ParentMod.Description);68 _writer.WriteString (_webMod.ParentMod.Description); 69 69 70 70 _writer.WriteValueSeparator (); 71 71 _writer.WritePropertyName ("author"); 72 JsonCommons.WriteStringOrNull (ref _writer,_webMod.ParentMod.Author);72 _writer.WriteString (_webMod.ParentMod.Author); 73 73 74 74 _writer.WriteValueSeparator (); 75 75 _writer.WritePropertyName ("version"); 76 JsonCommons.WriteStringOrNull (ref _writer,_webMod.ParentMod.VersionString);76 _writer.WriteString (_webMod.ParentMod.VersionString); 77 77 78 78 _writer.WriteValueSeparator (); 79 79 _writer.WritePropertyName ("website"); 80 JsonCommons.WriteStringOrNull (ref _writer,_webMod.ParentMod.Website);80 _writer.WriteString (_webMod.ParentMod.Website); 81 81 } 82 82 -
binary-improvements2/WebServer/src/WebAPI/APIs/Log.cs
r408 r434 55 55 56 56 writer.WriteRaw (jsonMsgKey); 57 writer.WriteString (logEntry. message);57 writer.WriteString (logEntry.Message); 58 58 59 59 writer.WriteRaw (jsonTypeKey); 60 writer.WriteString (logEntry. type.ToStringCached ());60 writer.WriteString (logEntry.Type.ToStringCached ()); 61 61 62 62 writer.WriteRaw (jsonTraceKey); 63 writer.WriteString (logEntry. trace);63 writer.WriteString (logEntry.Trace); 64 64 65 65 writer.WriteRaw (jsonIsotimeKey); 66 writer.WriteString (logEntry. isoTime);66 writer.WriteString (logEntry.IsoTime); 67 67 68 68 writer.WriteRaw (jsonUptimeKey); 69 writer.WriteString (logEntry. uptime.ToString ());69 writer.WriteString (logEntry.Uptime.ToString ()); 70 70 71 71 writer.WriteEndObject (); -
binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/RegisterUser.cs
r433 r434 9 9 using Webserver.UrlHandlers; 10 10 11 namespace Webserver.WebAPI.APIs {11 namespace Webserver.WebAPI.APIs.Permissions { 12 12 [UsedImplicitly] 13 13 public class RegisterUser : AbsRestApi { … … 27 27 28 28 if (string.IsNullOrEmpty (token)) { 29 SendE rrorResult(_context, HttpStatusCode.BadRequest, null, "NO_TOKEN");29 SendEmptyResponse (_context, HttpStatusCode.BadRequest, null, "NO_TOKEN"); 30 30 return; 31 31 } 32 32 33 33 if (!UserRegistrationTokens.TryValidate (token, out UserRegistrationTokens.RegistrationData regData)) { 34 SendE rrorResult(_context, HttpStatusCode.NotFound, null, "INVALID_OR_EXPIRED_TOKEN");34 SendEmptyResponse (_context, HttpStatusCode.NotFound, null, "INVALID_OR_EXPIRED_TOKEN"); 35 35 return; 36 36 } … … 50 50 51 51 protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) { 52 if (! TryGetJsonField (_jsonInput, "token", out string token)) {53 SendE rrorResult(_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_TOKEN");52 if (!JsonCommons.TryGetJsonField (_jsonInput, "token", out string token)) { 53 SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_TOKEN"); 54 54 return; 55 55 } 56 56 57 if (! TryGetJsonField (_jsonInput, "username", out string username)) {58 SendE rrorResult(_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_USERNAME");57 if (!JsonCommons.TryGetJsonField (_jsonInput, "username", out string username)) { 58 SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_USERNAME"); 59 59 return; 60 60 } 61 61 62 if (! TryGetJsonField (_jsonInput, "password", out string password)) {63 SendE rrorResult(_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_PASSWORD");62 if (!JsonCommons.TryGetJsonField (_jsonInput, "password", out string password)) { 63 SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_PASSWORD"); 64 64 return; 65 65 } 66 66 67 67 if (!UserRegistrationTokens.TryValidate (token, out UserRegistrationTokens.RegistrationData regData)) { 68 SendE rrorResult(_context, HttpStatusCode.Unauthorized, null, "INVALID_OR_EXPIRED_TOKEN");68 SendEmptyResponse (_context, HttpStatusCode.Unauthorized, null, "INVALID_OR_EXPIRED_TOKEN"); 69 69 return; 70 70 } 71 71 72 72 if (!userValidationRegex.IsMatch (username)) { 73 SendE rrorResult(_context, HttpStatusCode.Unauthorized, _jsonInputData, "INVALID_USERNAME");73 SendEmptyResponse (_context, HttpStatusCode.Unauthorized, _jsonInputData, "INVALID_USERNAME"); 74 74 return; 75 75 } 76 76 77 77 if (!passValidationRegex.IsMatch (password)) { 78 SendE rrorResult(_context, HttpStatusCode.Unauthorized, _jsonInputData, "INVALID_PASSWORD");78 SendEmptyResponse (_context, HttpStatusCode.Unauthorized, _jsonInputData, "INVALID_PASSWORD"); 79 79 return; 80 80 } … … 86 86 !PlatformUserIdentifierAbs.Equals (existingMapping.CrossPlatformUser, regData.CrossPlatformUserId)) { 87 87 // Username already in use by another player 88 SendE rrorResult(_context, HttpStatusCode.Unauthorized, _jsonInputData, "DUPLICATE_USERNAME");88 SendEmptyResponse (_context, HttpStatusCode.Unauthorized, _jsonInputData, "DUPLICATE_USERNAME"); 89 89 return; 90 90 } -
binary-improvements2/WebServer/src/WebAPI/APIs/WorldState/Animal.cs
r433 r434 4 4 using Webserver.LiveData; 5 5 6 namespace Webserver.WebAPI.APIs {6 namespace Webserver.WebAPI.APIs.WorldState { 7 7 [UsedImplicitly] 8 8 internal class Animal : AbsRestApi { -
binary-improvements2/WebServer/src/WebAPI/APIs/WorldState/Hostile.cs
r433 r434 4 4 using Webserver.LiveData; 5 5 6 namespace Webserver.WebAPI.APIs {6 namespace Webserver.WebAPI.APIs.WorldState { 7 7 [UsedImplicitly] 8 8 internal class Hostile : AbsRestApi { -
binary-improvements2/WebServer/src/WebAPI/APIs/WorldState/Player.cs
r433 r434 6 6 using Webserver.Permissions; 7 7 8 namespace Webserver.WebAPI.APIs {8 namespace Webserver.WebAPI.APIs.WorldState { 9 9 [UsedImplicitly] 10 10 public class Player : AbsRestApi { … … 216 216 217 217 protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) { 218 if (! TryGetJsonField (_jsonInput, "command", out string commandString)) {219 SendE rrorResult(_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_COMMAND");218 if (!JsonCommons.TryGetJsonField (_jsonInput, "command", out string commandString)) { 219 SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_COMMAND"); 220 220 return; 221 221 } … … 223 223 WebCommandResult.ResultType responseType = WebCommandResult.ResultType.Full; 224 224 225 if ( TryGetJsonField (_jsonInput, "format", out string formatString)) {225 if (JsonCommons.TryGetJsonField (_jsonInput, "format", out string formatString)) { 226 226 if (formatString.EqualsCaseInsensitive ("raw")) { 227 227 responseType = WebCommandResult.ResultType.Raw; … … 240 240 241 241 if (command == null) { 242 SendE rrorResult(_context, HttpStatusCode.NotFound, _jsonInputData, "UNKNOWN_COMMAND");242 SendEmptyResponse (_context, HttpStatusCode.NotFound, _jsonInputData, "UNKNOWN_COMMAND"); 243 243 return; 244 244 } … … 247 247 248 248 if (_context.PermissionLevel > commandPermissionLevel) { 249 SendE rrorResult(_context, HttpStatusCode.Forbidden, _jsonInputData, "NO_PERMISSION");249 SendEmptyResponse (_context, HttpStatusCode.Forbidden, _jsonInputData, "NO_PERMISSION"); 250 250 return; 251 251 } -
binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs
r426 r434 44 44 jsonDeserializeSampler.End (); 45 45 46 SendE rrorResult(_context, HttpStatusCode.BadRequest, null, "INVALID_BODY", e);46 SendEmptyResponse (_context, HttpStatusCode.BadRequest, null, "INVALID_BODY", e); 47 47 return; 48 48 } … … 53 53 case ERequestMethod.GET: 54 54 if (inputJson != null) { 55 SendE rrorResult(_context, HttpStatusCode.BadRequest, jsonInputData, "GET_WITH_BODY");55 SendEmptyResponse (_context, HttpStatusCode.BadRequest, jsonInputData, "GET_WITH_BODY"); 56 56 return; 57 57 } … … 60 60 return; 61 61 case ERequestMethod.POST: 62 if (! string.IsNullOrEmpty (_context.RequestPath)) {63 SendE rrorResult(_context, HttpStatusCode.BadRequest, jsonInputData, "POST_WITH_ID");62 if (!AllowPostWithId && !string.IsNullOrEmpty (_context.RequestPath)) { 63 SendEmptyResponse (_context, HttpStatusCode.BadRequest, jsonInputData, "POST_WITH_ID"); 64 64 return; 65 65 } 66 66 67 67 if (inputJson == null) { 68 SendE rrorResult(_context, HttpStatusCode.BadRequest, null, "POST_WITHOUT_BODY");68 SendEmptyResponse (_context, HttpStatusCode.BadRequest, null, "POST_WITHOUT_BODY"); 69 69 return; 70 70 } … … 74 74 case ERequestMethod.PUT: 75 75 if (string.IsNullOrEmpty (_context.RequestPath)) { 76 SendE rrorResult(_context, HttpStatusCode.BadRequest, jsonInputData, "PUT_WITHOUT_ID");76 SendEmptyResponse (_context, HttpStatusCode.BadRequest, jsonInputData, "PUT_WITHOUT_ID"); 77 77 return; 78 78 } 79 79 80 80 if (inputJson == null) { 81 SendE rrorResult(_context, HttpStatusCode.BadRequest, null, "PUT_WITHOUT_BODY");81 SendEmptyResponse (_context, HttpStatusCode.BadRequest, null, "PUT_WITHOUT_BODY"); 82 82 return; 83 83 } … … 87 87 case ERequestMethod.DELETE: 88 88 if (string.IsNullOrEmpty (_context.RequestPath)) { 89 SendE rrorResult(_context, HttpStatusCode.BadRequest, jsonInputData, "DELETE_WITHOUT_ID");89 SendEmptyResponse (_context, HttpStatusCode.BadRequest, jsonInputData, "DELETE_WITHOUT_ID"); 90 90 return; 91 91 } 92 92 93 93 if (inputJson != null) { 94 SendE rrorResult(_context, HttpStatusCode.BadRequest, null, "DELETE_WITH_BODY");94 SendEmptyResponse (_context, HttpStatusCode.BadRequest, null, "DELETE_WITH_BODY"); 95 95 return; 96 96 } … … 99 99 return; 100 100 default: 101 SendE rrorResult(_context, HttpStatusCode.BadRequest, null, "INVALID_METHOD");101 SendEmptyResponse (_context, HttpStatusCode.BadRequest, null, "INVALID_METHOD"); 102 102 return; 103 103 } 104 104 } catch (Exception e) { 105 SendE rrorResult(_context, HttpStatusCode.InternalServerError, jsonInputData, "ERROR_PROCESSING", e);105 SendEmptyResponse (_context, HttpStatusCode.InternalServerError, jsonInputData, "ERROR_PROCESSING", e); 106 106 } 107 107 } 108 108 109 109 protected virtual void HandleRestGet (RequestContext _context) { 110 SendE rrorResult(_context, HttpStatusCode.MethodNotAllowed, null, "Unsupported");110 SendEmptyResponse (_context, HttpStatusCode.MethodNotAllowed, null, "Unsupported"); 111 111 } 112 112 113 113 protected virtual void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) { 114 SendE rrorResult(_context, HttpStatusCode.MethodNotAllowed, _jsonInputData, "Unsupported");114 SendEmptyResponse (_context, HttpStatusCode.MethodNotAllowed, _jsonInputData, "Unsupported"); 115 115 } 116 116 117 117 protected virtual void HandleRestPut (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) { 118 SendE rrorResult(_context, HttpStatusCode.MethodNotAllowed, _jsonInputData, "Unsupported");118 SendEmptyResponse (_context, HttpStatusCode.MethodNotAllowed, _jsonInputData, "Unsupported"); 119 119 } 120 120 121 121 protected virtual void HandleRestDelete (RequestContext _context) { 122 SendE rrorResult(_context, HttpStatusCode.MethodNotAllowed, null, "Unsupported");122 SendEmptyResponse (_context, HttpStatusCode.MethodNotAllowed, null, "Unsupported"); 123 123 } 124 124 … … 126 126 AdminWebModules.WebModule module = AdminWebModules.Instance.GetModule (CachedApiModuleName); 127 127 128 if (module.LevelPerMethod != null) { 129 int perMethodLevel = module.LevelPerMethod [(int)_context.Method]; 130 if (perMethodLevel == AdminWebModules.MethodLevelNotSupported) { 131 return false; 132 } 128 if (module.LevelPerMethod == null) { 129 return module.LevelGlobal >= _context.PermissionLevel; 130 } 133 131 134 if (perMethodLevel != AdminWebModules.MethodLevelInheritGlobal) { 135 return perMethodLevel >= _context.PermissionLevel; 136 } 132 int perMethodLevel = module.LevelPerMethod [(int)_context.Method]; 133 if (perMethodLevel == AdminWebModules.MethodLevelNotSupported) { 134 return false; 135 } 136 137 if (perMethodLevel != AdminWebModules.MethodLevelInheritGlobal) { 138 return perMethodLevel >= _context.PermissionLevel; 137 139 } 138 140 139 141 return module.LevelGlobal >= _context.PermissionLevel; 140 142 } 143 144 protected virtual bool AllowPostWithId => false; 141 145 142 146 /// <summary> … … 154 158 #region Helpers 155 159 156 protected static readonly byte[] JsonEmptyData;157 158 static AbsRestApi () {159 JsonWriter writer = new JsonWriter ();160 writer.WriteBeginArray ();161 writer.WriteEndArray ();162 JsonEmptyData = writer.ToUtf8ByteArray ();163 }164 165 160 protected static void PrepareEnvelopedResult (out JsonWriter _writer) { 166 161 WebUtils.PrepareEnvelopedResult (out _writer); … … 173 168 } 174 169 175 protected static void SendE rrorResult (RequestContext _context, HttpStatusCode _statusCode, byte[] _jsonInputData = null, string _errorCode = null, Exception _exception = null) {170 protected static void SendEmptyResponse (RequestContext _context, HttpStatusCode _statusCode = HttpStatusCode.OK, byte[] _jsonInputData = null, string _errorCode = null, Exception _exception = null) { 176 171 PrepareEnvelopedResult (out JsonWriter writer); 177 writer.WriteRaw ( JsonEmptyData);172 writer.WriteRaw (WebUtils.JsonEmptyData); 178 173 SendEnvelopedResult (_context, ref writer, _statusCode, _jsonInputData, _errorCode, _exception); 179 174 } 180 181 protected static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out int _value) {182 _value = default;183 184 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {185 return false;186 }187 188 if (fieldNode is not double value) {189 return false;190 }191 192 try {193 _value = (int)value;194 return true;195 } catch (Exception) {196 return false;197 }198 }199 200 protected static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out double _value) {201 _value = default;202 203 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {204 return false;205 }206 207 if (fieldNode is not double value) {208 return false;209 }210 211 try {212 _value = value;213 return true;214 } catch (Exception) {215 return false;216 }217 }218 219 protected static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out string _value) {220 _value = default;221 222 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {223 return false;224 }225 226 if (fieldNode is not string value) {227 return false;228 }229 230 try {231 _value = value;232 return true;233 } catch (Exception) {234 return false;235 }236 }237 238 175 239 176 #endregion -
binary-improvements2/WebServer/src/WebAPI/JsonCommons.cs
r426 r434 1 1 using System; 2 using System.Collections.Generic; 3 using System.Globalization; 2 4 using UnityEngine; 3 5 using Utf8Json; … … 57 59 } 58 60 61 public static bool TryReadPlatformUserIdentifier (IDictionary<string, object> _jsonInput, out PlatformUserIdentifierAbs _userIdentifier) { 62 if (TryGetJsonField (_jsonInput, "combinedString", out string combinedString)) { 63 _userIdentifier = PlatformUserIdentifierAbs.FromCombinedString (combinedString, false); 64 if (_userIdentifier != null) { 65 return true; 66 } 67 } 68 69 if (!TryGetJsonField (_jsonInput, "platformId", out string platformId)) { 70 _userIdentifier = default; 71 return false; 72 } 73 74 if (!TryGetJsonField (_jsonInput, "userId", out string userId)) { 75 _userIdentifier = default; 76 return false; 77 } 78 79 _userIdentifier = PlatformUserIdentifierAbs.FromPlatformAndId (platformId, userId, false); 80 return _userIdentifier != null; 81 } 82 59 83 public static void WriteDateTime (ref JsonWriter _writer, DateTime _dateTime) { 60 84 _writer.WriteString (_dateTime.ToString ("o")); 61 85 } 62 86 63 public static void WriteStringOrNull (ref JsonWriter _writer, string _string) { 64 if (_string == null) { 65 _writer.WriteNull (); 66 } else { 67 _writer.WriteString (_string); 87 public static bool TryReadDateTime (IDictionary<string, object> _jsonInput, string _fieldName, out DateTime _result) { 88 _result = default; 89 90 if (!TryGetJsonField (_jsonInput, _fieldName, out string dateTimeString)) { 91 return false; 92 } 93 94 return DateTime.TryParse (dateTimeString, null, DateTimeStyles.RoundtripKind, out _result); 95 } 96 97 98 public static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out int _value) { 99 _value = default; 100 101 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) { 102 return false; 103 } 104 105 if (fieldNode is not double value) { 106 return false; 107 } 108 109 try { 110 _value = (int)value; 111 return true; 112 } catch (Exception) { 113 return false; 114 } 115 } 116 117 public static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out double _value) { 118 _value = default; 119 120 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) { 121 return false; 122 } 123 124 if (fieldNode is not double value) { 125 return false; 126 } 127 128 try { 129 _value = value; 130 return true; 131 } catch (Exception) { 132 return false; 133 } 134 } 135 136 public static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out string _value) { 137 _value = default; 138 139 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) { 140 return false; 141 } 142 143 if (fieldNode is not string value) { 144 return false; 145 } 146 147 try { 148 _value = value; 149 return true; 150 } catch (Exception) { 151 return false; 152 } 153 } 154 155 public static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, 156 out IDictionary<string, object> _value) { 157 _value = default; 158 159 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) { 160 return false; 161 } 162 163 if (fieldNode is not IDictionary<string, object> value) { 164 return false; 165 } 166 167 try { 168 _value = value; 169 return true; 170 } catch (Exception) { 171 return false; 68 172 } 69 173 }
Note:
See TracChangeset
for help on using the changeset viewer.