using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using AllocsFixes.JSON; using AllocsFixes.PersistentData; using Webserver; using Webserver.Permissions; using Webserver.WebAPI; namespace AllocsFixes.WebAPIs { public class GetPlayerList : AbsWebAPI { private static readonly Regex numberFilterMatcher = new Regex (@"^(>=|=>|>|<=|=<|<|==|=)?\s*([0-9]+(\.[0-9]*)?)$"); #if ENABLE_PROFILER private static readonly UnityEngine.Profiling.CustomSampler jsonSerializeSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Build"); #endif public override void HandleRequest (RequestContext _context) { AdminTools admTools = GameManager.Instance.adminTools; PlatformUserIdentifierAbs userId = _context.Connection?.UserId; bool bViewAll = PermissionUtils.CanViewAllPlayers (_context.PermissionLevel); // TODO: Sort (and filter?) prior to converting to JSON ... hard as how to get the correct column's data? (i.e. column name matches JSON object field names, not source data) int rowsPerPage = 25; if (_context.Request.QueryString ["rowsperpage"] != null) { int.TryParse (_context.Request.QueryString ["rowsperpage"], out rowsPerPage); } int page = 0; if (_context.Request.QueryString ["page"] != null) { int.TryParse (_context.Request.QueryString ["page"], out page); } int firstEntry = page * rowsPerPage; Players playersList = PersistentContainer.Instance.Players; List playerList = new List (); #if ENABLE_PROFILER jsonSerializeSampler.Begin (); #endif foreach (KeyValuePair kvp in playersList.Dict) { Player p = kvp.Value; if (bViewAll || p.InternalId.Equals (userId)) { JSONObject pos = new JSONObject (); pos.Add ("x", new JSONNumber (p.LastPosition.x)); pos.Add ("y", new JSONNumber (p.LastPosition.y)); pos.Add ("z", new JSONNumber (p.LastPosition.z)); JSONObject pJson = new JSONObject (); pJson.Add ("steamid", new JSONString (kvp.Value.PlatformId.CombinedString)); pJson.Add ("crossplatformid", new JSONString (kvp.Value.CrossPlatformId?.CombinedString ?? "")); pJson.Add ("entityid", new JSONNumber (p.EntityID)); pJson.Add ("ip", new JSONString (p.IP)); pJson.Add ("name", new JSONString (p.Name)); pJson.Add ("online", new JSONBoolean (p.IsOnline)); pJson.Add ("position", pos); pJson.Add ("totalplaytime", new JSONNumber (p.TotalPlayTime)); pJson.Add ("lastonline", new JSONString (p.LastOnline.ToUniversalTime ().ToString ("yyyy-MM-ddTHH:mm:ssZ"))); pJson.Add ("ping", new JSONNumber (p.IsOnline ? p.ClientInfo.ping : -1)); JSONBoolean banned = admTools != null ? new JSONBoolean (admTools.Blacklist.IsBanned (kvp.Key, out _, out _)) : new JSONBoolean (false); pJson.Add ("banned", banned); playerList.Add (pJson); } } #if ENABLE_PROFILER jsonSerializeSampler.End (); #endif IEnumerable list = playerList; foreach (string key in _context.Request.QueryString.AllKeys) { if (!string.IsNullOrEmpty (key) && key.StartsWith ("filter[")) { string filterCol = key.Substring (key.IndexOf ('[') + 1); filterCol = filterCol.Substring (0, filterCol.Length - 1); string filterVal = _context.Request.QueryString.Get (key).Trim (); list = ExecuteFilter (list, filterCol, filterVal); } } int totalAfterFilter = list.Count (); foreach (string key in _context.Request.QueryString.AllKeys) { if (!string.IsNullOrEmpty (key) && key.StartsWith ("sort[")) { string sortCol = key.Substring (key.IndexOf ('[') + 1); sortCol = sortCol.Substring (0, sortCol.Length - 1); string sortVal = _context.Request.QueryString.Get (key); list = ExecuteSort (list, sortCol, sortVal == "0"); } } list = list.Skip (firstEntry); list = list.Take (rowsPerPage); JSONArray playersJsResult = new JSONArray (); foreach (JSONObject jsO in list) { playersJsResult.Add (jsO); } JSONObject result = new JSONObject (); result.Add ("total", new JSONNumber (totalAfterFilter)); result.Add ("totalUnfiltered", new JSONNumber (playerList.Count)); result.Add ("firstResult", new JSONNumber (firstEntry)); result.Add ("players", playersJsResult); LegacyApiHelper.WriteJSON (_context.Response, result); } private IEnumerable ExecuteFilter (IEnumerable _list, string _filterCol, string _filterVal) { if (!_list.Any()) { return _list; } if (_list.First ().ContainsKey (_filterCol)) { Type colType = _list.First () [_filterCol].GetType (); if (colType == typeof (JSONNumber)) { return ExecuteNumberFilter (_list, _filterCol, _filterVal); } if (colType == typeof (JSONBoolean)) { bool value = StringParsers.ParseBool (_filterVal); return _list.Where (_line => ((JSONBoolean) _line [_filterCol]).GetBool () == value); } if (colType == typeof (JSONString)) { // regex-match whole ^string$, replace * by .*, ? by .?, + by .+ _filterVal = _filterVal.Replace ("*", ".*").Replace ("?", ".?").Replace ("+", ".+"); _filterVal = "^" + _filterVal + "$"; //Log.Out ("GetPlayerList: Filter on String with Regex '" + _filterVal + "'"); Regex matcher = new Regex (_filterVal, RegexOptions.IgnoreCase); return _list.Where (_line => matcher.IsMatch (((JSONString) _line [_filterCol]).GetString ())); } } return _list; } private IEnumerable ExecuteNumberFilter (IEnumerable _list, string _filterCol, string _filterVal) { // allow value (exact match), =, ==, >=, >, <=, < Match filterMatch = numberFilterMatcher.Match (_filterVal); if (filterMatch.Success) { double value = StringParsers.ParseDouble (filterMatch.Groups [2].Value); NumberMatchType matchType; double epsilon = value / 100000; switch (filterMatch.Groups [1].Value) { case "": case "=": case "==": matchType = NumberMatchType.Equal; break; case ">": matchType = NumberMatchType.Greater; break; case ">=": case "=>": matchType = NumberMatchType.GreaterEqual; break; case "<": matchType = NumberMatchType.Lesser; break; case "<=": case "=<": matchType = NumberMatchType.LesserEqual; break; default: matchType = NumberMatchType.Equal; break; } return _list.Where (delegate (JSONObject _line) { double objVal = ((JSONNumber) _line [_filterCol]).GetDouble (); switch (matchType) { case NumberMatchType.Greater: return objVal > value; case NumberMatchType.GreaterEqual: return objVal >= value; case NumberMatchType.Lesser: return objVal < value; case NumberMatchType.LesserEqual: return objVal <= value; case NumberMatchType.Equal: default: return NearlyEqual (objVal, value, epsilon); } }); } Log.Out ("GetPlayerList: ignoring invalid filter for number-column '{0}': '{1}'", _filterCol, _filterVal); return _list; } private IEnumerable ExecuteSort (IEnumerable _list, string _sortCol, bool _ascending) { if (_list.Count () == 0) { return _list; } if (_list.First ().ContainsKey (_sortCol)) { Type colType = _list.First () [_sortCol].GetType (); if (colType == typeof (JSONNumber)) { if (_ascending) { return _list.OrderBy (_line => ((JSONNumber) _line [_sortCol]).GetDouble ()); } return _list.OrderByDescending (_line => ((JSONNumber) _line [_sortCol]).GetDouble ()); } if (colType == typeof (JSONBoolean)) { if (_ascending) { return _list.OrderBy (_line => ((JSONBoolean) _line [_sortCol]).GetBool ()); } return _list.OrderByDescending (_line => ((JSONBoolean) _line [_sortCol]).GetBool ()); } if (_ascending) { return _list.OrderBy (_line => _line [_sortCol].ToString ()); } return _list.OrderByDescending (_line => _line [_sortCol].ToString ()); } return _list; } private bool NearlyEqual (double _a, double _b, double _epsilon) { double absA = Math.Abs (_a); double absB = Math.Abs (_b); double diff = Math.Abs (_a - _b); if (_a == _b) { return true; } if (_a == 0 || _b == 0 || diff < double.Epsilon) { return diff < _epsilon; } return diff / (absA + absB) < _epsilon; } private enum NumberMatchType { Equal, Greater, GreaterEqual, Lesser, LesserEqual } } }