From d2659ff68dfd3efbbcb34f8e2d0b25041572358c Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Fri, 17 Nov 2017 18:24:18 -0500 Subject: [PATCH] Major Performance Enhancements and Bug Fixes (#594) * Major Performance Enhancements and Bug Fixes * **NOTICE**: _This PR is extremely large. Due to the interdependencies, these code changes are being included as a single PR._ * **Local Caching of Database and Memcached Results** * Replaced PR #574 * Results from the database (MySQL) are stored and queried from the Cache service (Memcached). Results from Memcached are now stored locally within HHVM memory as well. * New class `Cache` has been added, with four methods: * `setCache()` * `getCache()` * `deleteCache()` * `flushCache()` * The new `Cache` class object is included as a static property of the `Model` class and all children. * Through the new `Cache` object, all `Model` sub-classes will automatically utilize the temporary HHVM-memory cache for results that have already been retrieved. * The `Cache` object is created and used throughout a single HHVM execution thread (user request). * This change results in a massive reduction in Memcached requests (over 99% - at scale), providing a significant improvement in scalability and performance. * The local cache can be deleted or flushed from the `Model` class or any child class with the usage of `deleteLocalCache()`. The `deleteLocalCache()` method works like `invalidateMCRecords()`. When called from a child class, a `$key` can be passed in, matching the MC key identifier used elsewhere, or if no `$key` is specified all of the local caches for that class will be deleted. If the calling class is `Model` then the entire local cache is flushed. * Some non-HTTP code has local cache values explicitly deleted, or the local cache completely flushed, as the execution thread is continuous: * Prevent `autorun.php` from storing timestamps in the local cache, forever (the script runs continuously). * Flush the local cache before the next cycle of `bases.php` to ensure the game is still running and the configuration of the bases has not changed (the script runs continuously). * Flush the local cache before the next import cycle of `liveimport.php` to ensure we get the up-to-date team and level data (the script runs continuously). * The `Cache` class is specifically separate from `Model` (as an independent class) so that other code may instantiate and utilize a temporary (request-exclusive) local-memory-based caching solution, with a common interface. The usage provides local caching without storing the data in MySQL, Memcached, or exposing it to other areas of the application. (For example, this is being utilized in some `Integration` code already.) * Implemented CR from PR #574. * Relevant: Issue #456 and Comment https://github.com/facebook/fbctf/issues/456#issuecomment-332952554 * **Blocking AJAX Requests** * Replaced PR #575 * Expansion and Bug Fixes of PR #565 * AJAX requests for the gameboard are now individually blocking. A new request will not be dispatched until the previous request has completed. * AJAX requests will individually stop subsequent requests on a hard-error. * The blocking of continuous AJAX requests, when the previous has not yet returned, or on a hard error, provides a modest performance benefit by not compounding the issue with more unfulfillable requests. * Forceful refreshes are still dispatched every 60 seconds, regardless of the blocking state on those requests. * Relevant: Issue #456 and Comment https://github.com/facebook/fbctf/issues/456#issuecomment-332952554 * **AJAX Endpoint Optimization** * Removed nested loops within multiple AJAX endpoints: * `map-data.php` * `country-data.php` * ` leaderboard.php` * All Attachments, including Link and Filename, are now cached and obtained through: `Attachment::genAllAttachmentsFileNamesLinks()`. * All Team names of those who have completed a level, are now cached and obtained through `MultiTeam::genCompletedLevelTeamNames()`. * All Levels and Country for map displays are cached and obtained through `Level::genAllLevelsCountryMap()` and `Country::genAllEnabledCountriesForMap()`. * Relevant: Issue #456 * **Memcached Cluster Support** * The platform now supports a cluster of Memcached nodes. * Configuration for the `MC_HOST` within the `settings.ini` file is now an array, instead of a single value: * `MC_HOST[] = 127.0.0.1` * Multiple Memcached servers can be configured by providing additional `MC_HOST` lines: * `MC_HOST[] = 1.2.3.4` * `MC_HOST[] = 5.6.7.8` * The platform uses a Write-Many Read-Once approach to the Memcached Cluster. Specifically, data is written to all of the configured Memcached nodes and then read from a single node at random. This approach ensures that all of the nodes stay in sync and up-to-date while providing a vital performance benefit to the more expensive and frequent operation of reading. * The existing `Model` methods (`setMCRecords()` and `invalidateMCRecords()`) all call and utilize the new cluster methods: * `writeMCCluster()` * `invalidateMCCluster()` * The flushing of Memcached has also been updated to support the multi-cluster approach: `flushMCCluster()`. * Note that the usage of a Memcached Cluster is seamless for administrators and users, and works in conjunction with the Local Cache. Also note, the platform works identically, for administrators and users, for both single-node and multi-node Memcached configurations. * The default configuration remains a single-node configuration. The utilization of a Memcached Cluster requires the following: * The configuration and deployment of multiple Memcached nodes (the `quick_setup install_multi_cache` or Memcached specific provision, will work). * The modification of `settings.ini` to include all of the desired Memcached hosts. * All Memcached hosts must be identically configured. * Usage of a Memcached Cluster is only recommended in the Multi-Server deployment modes. * Relevant: Issue #456 * **Load Balancing of Application Servers (HHVM)** * The platform now supports the ability to load balance multiple HHVM servers. * To facilitate the load balancing of the HHVM servers, the following changes were made: * Scripts (`autorun`, `progressive`, etc.) are now tracked on a per-server level, preventing multiple copies of the scripts from being executed on the HHVM servers. * Additional database verification on scoring events to prevent multiple captures. * Load Balancing of HHVM is only recommended in the Multi-Server deployment modes. * Relevant: Issue #456 * **Leaderboard Limits** * `MultiTeam::genLeaderboard()` now limits the total number of teams returned based on a configurable setting. * A new argument has been added to `MultiTeam::genLeaderboard()`: `limit`. This value, either `true` or `false`, indicates where the limit should be enforced, and defaults to `true`. * When the data is not cached, `MultiTeam::genLeaderboard()` will only build, cache, and return the number of teams needed to meet the limit. * When the data is already cached, `MultiTeam::genLeaderboard()` will ensure the limit value has not changed and returned the cached results. If the configured limit value has been changed, `MultiTeam::genLeaderboard()` will build, cache, and return the number based on the new limit. * The "Scoreboard" modal (found from the main gameboard) is a special case where all teams should be displayed. As such, the Scoreboard modal sets the `limit` value to `false` retuning all teams. This full leaderboard will be cached, but all other display limits are still enforced based on the configured limit. Once invalidated, the cached data will return to the limited subset. * Because a full leaderboard is not always cached, this does result in the first hit to the Scoreboard modal requiring a database hit. * A user, whose rank is above the limit, will have their rank shown to them as `$limit+`. For example, if the limit is set to `50` and the user's rank is above `50`, they would see: `51+` as their rank. * Overall, the caching of the Leaderboard, one of the more resource-intensive and frequent queries, resulted in significant performance gains. * The Leaderboard limit is configurable by administrators within the administrative interface. The default value is `50`. * Relevant: Issue #456 * **Activity Log Limits** * The Activity Log is now limited to the most recent `100` log entries. The value is not configurable. * The activity log is continually queried and contains a large amount of data, as such, it is a very resource-intensive request. * The limit on the results built, cached, and returned for the activity log provides a notable improvement in performance. * Relevant: Issue #456 * **Database Optimization** * Expansion of PR #564 * Added additional indexing of the database tables in the schema. * The additional indexing provides further performance improvements to the platform queries, especially those found in `MultiTeam` and those queries continually utilized as a result of the AJAX calls. * Relevant: Issue #456 and Comment https://github.com/facebook/fbctf/issues/456#issuecomment-332952554 * **Team and MultiTeam Performance Improvements** * Updated numerous `Team::genTeam()` calls to used the cached version: `MultiTeam::genTeam()`. * Optimized the database query within `MultiTeam::genFirstCapture()` to return the `team_id` and build the `Team` from the cache. * Optimized the database query within `MultiTeam::genCompletedLevel()` to return the `team_id` and build the `Team` from the cache. * Optimized the database query within `MultiTeam::genAllCompletedLevels()` to return the `team_id` and build the `Team` from the cache. * A full invalidation of the `MultiTeam` cache is no longer executed when a new team is created. Newly created teams will not have any valid scoring activity. Delaying the rebuild of the scoring related cache provides a modest performance improvement. The new team will not show up in certain areas (namely the full scoreboard) until they or someone else perform a scoring action. To ensure the team is properly functioning, following cache is specifically invalided on a new team creation: * `ALL_TEAMS` * `ALL_ACTIVE_TEAMS` * `ALL_VISIBLE_TEAMS` * `TEAMS_BY_LOGO` * Fixed an extremely rare race condition within `MultiTeam::genFirstCapture()`. * Relevant: Issue #456 * **Combined Awaitables** * Combined Awaitables which were not in nested loops. * Combined Awaitables found in some nested loops, where existing code provided a streamlined approach. * Given the lack of support for concurrent queries to a single database connection, some queries were combined via `multiQuery()` (in the case where the queries were modifying data within the database). TODO: Build and utilize additional `AsyncMysqlConnection` within the pool for suitable concurrent queries. * Annotated Awaitables within a nested loop for future optimization. * Relevant: Issue #577 * **Facebook and Google Login Integration** * Replaced PR #573 * The platform now supports Login via OAuth2 for Facebook and Google. When configured and enabled, users will have the option to link and login to their existing account with a Facebook or Google account. * Automated registration through Facebook or Google OAuth2 is now supported. When configured and enabled, users will have the option to register an account by using and linking an existing account with Facebook or Google. * New `configuration` options added to the database schema: * Added `facebook_login`. This configuration option is a toggleable setting to enable or disable login via Facebook. * Added `google_login`. This configuration option is a toggleable setting to enable or disable login via Google. * Added `facebook_registration`. This configuration option is a toggleable setting to enable or disable registration via Facebook. * Added `google_registration`. This configuration option is a toggleable setting to enable or disable registration via Google. * Added `registration_prefix`. This configuration option is a string that sets the prefix for the randomly generated username/team name for teams registered via (Facebook or Google) OAuth. * New Integration section within the Administrative interface allows for control over the Facebook and Google Login, Registration, and the automatic team name prefix option. * Overhauled the Login page to support the new Login buttons. Login page now displays appropriate messages based on the configuration of login. * Login form is dynamically generated, based on the configuration options and settings. * Overhauled the Registration page to support the new Registration buttons. The registration page now displays appropriate messages based on the configuration of registration. * The registration form is dynamically generated, based on the configuration options and settings. * Account Linking for Facebook sets both the Login OAuth values and the LiveSync values (single step for both). * Account Linking for Google sets both the Login OAuth values and the LiveSync values (single step for both). * Facebook Account linkage option has been added to the Account modal. * The Account modal now shows which accounts are already linked. * The Account modal will color-code the buttons on an error (red) and success (green). * New table "teams_oauth" has been added to handle the OAuth data for Facebook and Google account linkage. * New class `Integration` handles the linkage of Facebook or Google accounts with an FBCTF account (both Login OAuth values and the LiveSync values). The Integration class also includes the underlying methods for authentication in both the linkage and login routines and the OAuth registration process. * New URL endpoints have been created and simplified for the `Integration` actions: * New data endpoint `data/integration_login.php`. This endpoint accepts a type argument, currently supporting types of `facebook` and `google`. Through this endpoint, the login process is handled in conjunction with the Integration class. * The new callback URL for Facebook Login: `/data/integration_login.php?type=facebook` * The new callback URL for Google Login: `/data/integration_login.php?type=google` * New data endpoint data/integration_oauth.php. This endpoint accepts a type argument, currently supporting types of `facebook'`and `google`. Through this endpoint, the OAuth account linkage is handled in conjunction with the Integration class. * The new callback URL for Facebook linkage: /data/integration_login.php?type=facebook * The new callback URL for Google linkage: /data/integration_login.php?type=google * Old Google-specific endpoint (data/google_oauth.php) has been removed. * New Team class methods: `genAuthTokenExists()`, `genTeamFromOAuthToken()`, `genSetOAuthToken()`. * `Team::genAuthTokenExists()` allows an OAuth token to be verified. * `Team::genTeamFromOAuthToken()` returns a Team object based on the OAuth token supplied. * `Team::genSetOAuthToken()` sets the OAuth token for a team. * The `settings.ini` (including the packaged example file) and `Configuration` have methods to verify and return Facebook and Google API settings. * `Configuration::getFacebookOAuthSettingsExists()` verifies the Facebook API _App ID_ and _APP Secret_ are set in the `settings.ini` file. * `Configuration::getFacebookOAuthSettingsAppId()` returns the Facebook API _App ID_. * `Configuration::getFacebookOAuthSettingsAppSecret()` returns the Facebook API _App Secret_. * `Configuration::getGoogleOAuthFileExists()` verifies the Google API JSON file is set and exists in the `settings.ini` file. * `Configuration::getGoogleOAuthFile()` returns the filename for the Google API JSON file. * All of Facebook and Google API configuration values are cached (in Memcached) to prevent the repeated loading, reading, and parsing of the `settings.ini` file. * To use the new Facebook or Google integration the following must be completed: * A Facebook and/or Google Application must be created, and OAuth2 API keys must be obtained. * The API keys must be provided in the Settings.ini file. * Desired settings must be configured from within the administrative interface (Configuration) - the default has all integration turned off. * The Facebook OAuth code provides CSRF protection through the Graph SDK. * The Google OAuth code provides CSRF protection through the usage of the `integration_csrf_token` cookie and API state value. * Note: Facebook Login/Integration will not work in development mode - this is due to a pending issue in the Facebook Graph SDK (facebook/php-graph-sdk#853) utilization of the pending PR (facebook/php-graph-sdk#854) resolves this issue. Alternatively, the Integration with Facebook will work in production mode, the recommended mode for a live game. * Implemented CR from PR #573. * Relevant: PR #591 and PR #459. * **LiveSync API and LiveImport Script Update** * LiveSync has been updated to support and supply Facebook and Google OAuth output. All of the users LiveSync integrations (FBCTF, Facebook, and Google) are now provided through the API. As a result, so long as one of the three LiveSync methods are configured by the user (which happens automatically when linking an account to Facebook or Google) the data will become available through the LiveSync API. * LiveSync now includes a "general" type. The new `general` type output includes the scoring information using the local team name on the FBCTF instance. This new type is not for importation on another FBCTF instance but does provide the opportunity for third-parties to use the data for score tracking, metric collections, and displays. As such, this new LiveSync data allows the scoring data for a single FBCTF instance to be tracked. * The `liveimport.sh` script, used to import LiveSync API data, will ignore the new `general` LiveSync type. * Updated `Team::genLiveSyncKeyExists()` and `Team::genTeamFromLiveSyncKey()` to use the new Integration class methods: `Integration::genFacebookThirdPartyExists)` and `Integration::genFacebookThirdPartyEmail()`. * Within the `liveimport.sh` script: when the type is `facebook_oauth`, `Team::genLiveSyncKeyExists()` and `Team::genTeamFromLiveSyncKey()` properly use the Facebook `third_party_id`. * `Integration::genFacebookThirdPartyExists()` and `Integration::genFacebookThirdPartyEmail()` query the Facebook API for the coorosponding user, storing the results in a temporary HHVM-memory cache, via the `Cache` class. * Given that `liveimport.sh` now needs to query the Facebook API for any `facebook_oauth` typed items, the script will utilize the HHVM-memory cache of `Integration` to limit the number of hits to the Facebook API. * The `liveimport.sh` script now includes the `Cache` class and the Facebook Graph SDK. * Relevant: PR #459. * **Error and Exception Handling** * All Exceptions, including Redirect Exceptions, are now caught. * The NGINX configuration has been updated to catch errors from HHVM (FastCGI) and return `error.php`. * The `error.php` page has been updated with a themed error page. * The `error.php` page will redirect to `index.php?page=error` so long as `index.php?page=error` is not generating any HTTP errors. If an error is detected on `index.php?page=error` then no redirect will occur. The verification of the HTTP status ensures no redirect loops occur. * The `DataController` class now includes a `sendData()` method to catch errors and exceptions. `DataController` children classes now utilize `sendData()` instead of outputing their results directly. * On Exception within an AJAX request, an empty JSON array is returned. This empty array prevents client-side errors. * The `ModuleController` class now includes a `sendRender()` method to catch errors and exceptions. `ModuleController` children classes now utilize `sendRender()` instead of outputing their results directly. * On Exception within a Module request, an empty string is returned. This empty string prevents client-side and front-end errors. * A new AJAX endpoint has been added: `/data/session.php`. The response of the endpoint is used to determine if the user's session is still active. If a user's session is no longer active, they will be redirected from the gameboard to the login page. This redirection ensures that they do not continually perform AJAX requests. * Custom HTTP headers are used to monitor AJAX responses: * The Login page now includes a custom HTTP header: `Login-Page`. * The Error page now includes a custom HTTP header: `Error-Page`. * The custom HTTP headers are used client-side (JS) to determine if a request or page rendered an error or requires authentication. * Exception log outputs now include additional information on which Exception was thrown. * Users should no longer directly receive an HTTP 500. * These Exception changes prevent the error logs from being filled with unauthenticated requests. The changes also provide a user-friendly experience when things malfunction or a user needs to reauthenticate. * Relevant: #563 * **Team Account Modal Update** * Users can now change their team name from within the Account modal. * The account Modal now contains the following options: * Team Name * Facebook Account Linkage * Google Account Linkage * FBCTF LiveSync Authentication * Relevant: PR #459. * **Non-Visible/Inactive Team Update** * Ensure that non-visible or inactive team do not show up for any other users. * Non-Visible/Inactive teams are not awarded as the "first capture." * Non-Visible/Inactive teams do not show in the "captured by" list. * Countries will not show as captured (for other teams) if only captured by a Non-Visible/Inactive team. * Activity Log entries for a Non-Visible or Inactive team are not included in the activity log for other users. * Updated `ScoreLog::genAllPreviousScore()` and `ScoreLog::genPreviousScore()` to only include Visible and Active teams, or the user's own team. * Teams who are Non-Visible or Inactive will have a rank of "N/A." * Relevant: PR #513 * **Mobile Page Update** * The mobile page is shown when a user's window has a resolution under `960px`. While this is geared towards mobile users, it can happen when the window size on a non-mobile device is too small. * The mobile page now includes a "Refresh" button, which will reload the page. * The mobile page will refresh, attempting to re-render correctly, after `2` seconds. * If a user resizes their window to a larger size, they should reload into a properly displayed screen, and not the mobile warning. * **Login and Registration JS Fixes** * Consistently corrected the usage of `teamname` and `team_name` across PHP and JS code. * Ensured that all JavaScript is using `team_name`. * Ensured that all PHP is using `team_name` when interacting with JS. * Updated the input filters within PHP when retrieving input for the team name, using `team_name`. * Updated Login errors to highlight the username and password fields. * Relevant: Issue #571, Issue #558, Issue #521, PR #592, and PR #523 * **System Statistics JSON Endpoint** * A new administrative-only JSON endpoint has been added that provides statistical data about the platform and game. * The endpoint is found at `/data/stata.php`. Access to the endpoint requires an authenticated administrative session. * The endpoint provides the following information: * Number of Teams (`teams`) * Number of Sessions (`sessions`) * Total Number of Levels (`levels`) * Number of Active Levels (`active_levels`) * Number of Hints (`hints`) * Number of Captures (`captures`) * `AsyncMysqlConnectionPool` Statistics (`database`) * Created Connections (`created_pool_connections`) * Destroyed Connections (`destroyed_pool_connections`) * Connection Requests (`connections_requested`) * Pool Hits (`pool_hits`) * Pool Misses (`pool_misses`) * `Memcached` Statistics (`memcached`) * Node Address * Node Address:Port * Process ID (`pid`) * Uptime (`uptime`) * Threads (`threads`) * Timestamp (`time`) * Size of Pointer (`pointer_size`) * Total User Time for Memcached Process (`rusage_user_seconds`) * Total User Time for Memcached Process (`rusage_user_microseconds`) * Total System Time for Memcached Process (`rusage_system_seconds`) * Total System Time for Memcached Process (`rusage_system_microseconds`) * Current Items in Cache (`curr_items`) * Total Items in Cache (`total_items`) * Max Bytes Limit (`limit_maxbytes`) * Number of Current Connections (`curr_connections`) * Number of Total Connections (`total_connections`) * Number of Current Connection Structures Allocated (`connection_structures`) * Number of Bytes Used (`bytes`) * Total Number of Cache Get Requests (`cmd_get`) * Total Number of Cache Set Requests (`cmd_set`) * Total Number Successful of Cache Retrievals (`get_hits`) * Total Number Unsuccessful of Cache Retrievals (`get_ misses`) * Total Number of Cache Evictions (`evictions`) * Total Number of Bytes Read (`bytes_read`) * Total Number of Bytes Written (`bytes_writter`) * Memcached Version (`version`) * System Load Statistics (`load`) * One Minute Average (`0`) * Five Minute Average (`1`) * Fifteen Minute Average (`2`) * System CPU Utilization (`load`) * Userspace Utilization Percentage (`user`) * Nice Utilization Percentage (`nice`) * System Utilization Percentage (`sys`) * Idle Percentage (`idle`) * The endpoint provides current data and can be pooled/ingested for historical data reporting. * For more information on the `AsyncMysqlConnectionPool` statistics, please see: https://docs.hhvm.com/hack/reference/class/AsyncMysqlConnection/ and https://docs.hhvm.com/hack/reference/class/AsyncMysqlConnectionPool/getPoolStats/ * For more information on the `Memcached` statistics, please see: https://github.com/memcached/memcached/blob/master/doc/protocol.txt and https://secure.php.net/manual/en/memcached.getstats.php * **Miscellaneous Changes** * Added `Announcement` and `ActivityLog` to `autorun.php`. * Added `Announcement` and `ActivityLog` to `bases.php`. * Added/Updated UTF-8 encoding on various user-controlled values, such as team name. * Changed the "Sign Up" link to a button on the login page. * Allow any Logo to be re-used once all logos are in use. * Invalidate Scores and Hint cache when a Team is deleted. * Reverify the game status (running or stopped) before the next cycle of base scoring in `bases.php`. * Allow the game to stop even if some scripts (`autorun`, `bases`, `progressive`, etc.) are not running. * Fixed a bug where teams with a custom logo could not be edited by an administrator. * Added "Reset Schedule" button to administrative interface to completely remove a previously set game schedule. The game schedule can only be reset if the game is not running. Otherwise, the existing schedule must be modified. * Moved "Begin Game," "Pause Game," and "End Game" outside of the scrollable admin list into a new fixed pane below the navigation list. * Formatted all code files as part of this PR. * Updated ActivityLog to delete entries when deleting a team. * Updated PHPUnit tests based on the new changes. --- composer.json | 1 + composer.lock | 63 +- database/countries.sql | 6 +- database/schema.sql | 82 +- database/test_schema.sql | 88 +- extra/nginx.conf | 1 + extra/nginx/nginx.conf | 2 + extra/settings.ini.example | 4 +- src/Db.php | 5 + src/Router.php | 2 +- src/SessionUtils.php | 4 +- src/controllers/AdminController.php | 446 ++++++--- src/controllers/Controller.php | 32 +- src/controllers/GameboardController.php | 2 +- src/controllers/IndexController.php | 924 +++++++++++++----- src/controllers/ajax/AdminAjaxController.php | 25 +- src/controllers/ajax/GameAjaxController.php | 30 +- src/controllers/ajax/IndexAjaxController.php | 65 +- .../modals/ActionModalController.php | 133 ++- .../modals/CountryModalController.php | 38 +- .../modals/ScoreboardModalController.php | 16 +- src/data/announcements.php | 12 +- src/data/attachment.php | 9 +- src/data/captures.php | 28 + src/data/command-line.php | 27 +- src/data/configuration.php | 42 +- src/data/controller.php | 23 + src/data/country-data.php | 107 +- src/data/google_oauth.php | 128 --- src/data/integration_login.php | 57 ++ src/data/integration_oauth.php | 143 +++ src/data/leaderboard.php | 26 +- src/data/livesync.php | 111 ++- src/data/map-data.php | 48 +- src/data/scores.php | 16 +- src/data/session.php | 19 + src/data/stats.php | 83 ++ src/data/teams.php | 40 +- src/error.php | 42 +- src/inc/gameboard/listview.php | 30 +- .../gameboard/modules/activity-viewmode.php | 10 +- src/inc/gameboard/modules/activity.php | 29 +- src/inc/gameboard/modules/announcements.php | 14 +- src/inc/gameboard/modules/controller.php | 14 + src/inc/gameboard/modules/filter.php | 14 +- src/inc/gameboard/modules/game-clock.php | 27 +- .../modules/leaderboard-viewmode.php | 4 +- src/inc/gameboard/modules/leaderboard.php | 33 +- src/inc/gameboard/modules/teams.php | 29 +- src/index.php | 4 +- src/models/ActivityLog.php | 40 +- src/models/Announcement.php | 8 +- src/models/Attachment.php | 171 +++- src/models/Cache.php | 31 + src/models/Configuration.php | 150 ++- src/models/Control.php | 350 +++---- src/models/Country.php | 16 +- src/models/Integration.php | 534 ++++++++++ src/models/Level.php | 146 ++- src/models/Link.php | 72 +- src/models/Logo.php | 19 +- src/models/Model.php | 137 ++- src/models/MultiTeam.php | 243 ++++- src/models/ScoreLog.php | 218 ++++- src/models/Session.php | 53 +- src/models/Team.php | 338 +++++-- src/models/Token.php | 5 +- src/scripts/autorun.php | 3 + src/scripts/bases.php | 17 +- src/scripts/liveimport.php | 47 +- src/scripts/progressive.php | 1 + src/static/css/scss/_admin.scss | 11 +- src/static/css/scss/_modals.scss | 5 + src/static/js/admin.js | 14 +- src/static/js/app.js | 11 +- src/static/js/fb-ctf.js | 706 ++++++++----- src/static/js/index.js | 8 +- src/static/svg/map/world.php | 16 +- src/xhp/Custombranding.php | 8 +- src/xhp/Fbbranding.php | 3 +- tests/_files/seed.xml | 6 + tests/models/ConfigurationTest.php | 2 +- tests/models/ScoreLogTest.php | 16 +- tests/models/SessionTest.php | 2 +- 84 files changed, 4919 insertions(+), 1626 deletions(-) create mode 100644 src/data/captures.php delete mode 100644 src/data/google_oauth.php create mode 100644 src/data/integration_login.php create mode 100644 src/data/integration_oauth.php create mode 100644 src/data/session.php create mode 100644 src/data/stats.php create mode 100644 src/inc/gameboard/modules/controller.php create mode 100644 src/models/Cache.php create mode 100644 src/models/Integration.php diff --git a/composer.json b/composer.json index 0dc4d333..70eb026d 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "license": "CC-BY-NC-4.0", "require": { "facebook/xhp-lib": "2.x", + "facebook/graph-sdk": "5.x", "google/apiclient": "^2.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index ce54b8ed..64d2fa25 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,67 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "7fa4023de48d9c453ad8eaa93913ae96", - "content-hash": "a8b3ade722987ad56f29a0fc697a5c70", + "hash": "98f4d4750954ef68e0ab70174023f199", + "content-hash": "d462af7ff5694e2192b54e1ffd67c264", "packages": [ + { + "name": "facebook/graph-sdk", + "version": "5.6.1", + "source": { + "type": "git", + "url": "https://github.com/facebook/php-graph-sdk.git", + "reference": "2f9639c15ae043911f40ffe44080b32bac2c5280" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facebook/php-graph-sdk/zipball/2f9639c15ae043911f40ffe44080b32bac2c5280", + "reference": "2f9639c15ae043911f40ffe44080b32bac2c5280", + "shasum": "" + }, + "require": { + "php": "^5.4|^7.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "~5.0", + "mockery/mockery": "~0.8", + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "guzzlehttp/guzzle": "Allows for implementation of the Guzzle HTTP client", + "paragonie/random_compat": "Provides a better CSPRNG option in PHP 5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Facebook\\": "src/Facebook/" + }, + "files": [ + "src/Facebook/polyfills.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Facebook Platform" + ], + "authors": [ + { + "name": "Facebook", + "homepage": "https://github.com/facebook/php-graph-sdk/contributors" + } + ], + "description": "Facebook SDK for PHP", + "homepage": "https://github.com/facebook/php-graph-sdk", + "keywords": [ + "facebook", + "sdk" + ], + "time": "2017-08-16 17:28:07" + }, { "name": "facebook/xhp-lib", "version": "v2.3.2", @@ -133,6 +191,7 @@ "TypeAssert", "hack" ], + "abandoned": "hhvm/type-assert", "time": "2017-02-15 02:26:23" }, { diff --git a/database/countries.sql b/database/countries.sql index 8bf03ef4..4dba1bba 100644 --- a/database/countries.sql +++ b/database/countries.sql @@ -12,7 +12,9 @@ CREATE TABLE `countries` ( `enabled` tinyint(1) DEFAULT 0, `d` text DEFAULT NULL, `transform` text DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `iso_code` (`iso_code`), + KEY `enabled` (`enabled`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -298,4 +300,4 @@ UPDATE `countries` SET enabled = 1 WHERE iso_code = "ZA"; UPDATE `countries` SET enabled = 1 WHERE iso_code = "ZM"; UPDATE `countries` SET enabled = 1 WHERE iso_code = "ZW"; /*!40000 ALTER TABLE `countries` ENABLE KEYS */; -UNLOCK TABLES; +UNLOCK TABLES; \ No newline at end of file diff --git a/database/schema.sql b/database/schema.sql index 82a2966b..3c80338e 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -36,7 +36,7 @@ CREATE TABLE `levels` ( `id` int(11) NOT NULL AUTO_INCREMENT, `active` tinyint(1) NOT NULL, `type` varchar(4) NOT NULL, - `title` text NOT NULL, + `title` varchar(255) NOT NULL, `description` text NOT NULL, `entity_id` int(11) NOT NULL, `category_id` int(11) NOT NULL, @@ -48,7 +48,9 @@ CREATE TABLE `levels` ( `hint` text NOT NULL, `penalty` int(11) NOT NULL, `created_ts` timestamp NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `entity_id` (`entity_id`), + KEY `active` (`active`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -61,7 +63,7 @@ DROP TABLE IF EXISTS `categories`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `categories` ( `id` int(11) NOT NULL AUTO_INCREMENT, - `category` text NOT NULL, + `category` varchar(255) NOT NULL, `protected` tinyint(1) NOT NULL, `created_ts` timestamp NOT NULL DEFAULT 0, PRIMARY KEY (`id`) @@ -116,8 +118,8 @@ DROP TABLE IF EXISTS `teams`; CREATE TABLE `teams` ( `id` int(11) NOT NULL AUTO_INCREMENT, `active` tinyint(1) NOT NULL DEFAULT 1, - `name` text NOT NULL, - `password_hash` text NOT NULL, + `name` varchar(255) NOT NULL, + `password_hash` varchar(255) NOT NULL, `points` int(11) NOT NULL DEFAULT 0, `last_score` timestamp NOT NULL, `logo` text NOT NULL, @@ -125,7 +127,9 @@ CREATE TABLE `teams` ( `protected` tinyint(1) NOT NULL DEFAULT 0, `visible` tinyint(1) NOT NULL DEFAULT 1, `created_ts` timestamp NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `visible` (`visible`), + KEY `active` (`active`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -146,6 +150,22 @@ CREATE TABLE `livesync` ( ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `teams_oauth` +-- + +DROP TABLE IF EXISTS `teams_oauth`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `teams_oauth` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `type` text NOT NULL, + `team_id` int(11) NOT NULL, + `token` text NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `teams_data` -- @@ -156,10 +176,11 @@ DROP TABLE IF EXISTS `teams_data`; CREATE TABLE `teams_data` ( `id` int(11) NOT NULL AUTO_INCREMENT, `team_id` int(11) NOT NULL, - `name` text NOT NULL, - `email` text NOT NULL, + `name` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, `created_ts` timestamp NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `team_id` (`team_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -172,13 +193,14 @@ DROP TABLE IF EXISTS `sessions`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `sessions` ( `id` int(11) NOT NULL AUTO_INCREMENT, - `cookie` text NOT NULL, + `cookie` varchar(200) NOT NULL, `data` text NOT NULL, `team_id` int(11) NOT NULL, `created_ts` timestamp NOT NULL DEFAULT 0, `last_access_ts` timestamp NOT NULL, - `last_page_access` text NOT NULL, - PRIMARY KEY (`id`) + `last_page_access` varchar(200) NOT NULL, + PRIMARY KEY (`id`), + KEY `cookie` (`cookie`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -215,10 +237,16 @@ INSERT INTO `configuration` (field, value, description) VALUES("auto_announce", INSERT INTO `configuration` (field, value, description) VALUES("progressive_cycle", "300", "(Integer) Frequency to take progressive scoreboard in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("bases_cycle", "5", "(Integer) Frequency to score base levels in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("autorun_cycle", "30", "(Integer) Frequency to cycle autorun in seconds"); +INSERT INTO `configuration` (field, value, description) VALUES("gameboard_cycle", "5", "(Integer) Frequency to cycle gameboard in seconds"); +INSERT INTO `configuration` (field, value, description) VALUES("conf_cycle", "10", "(Integer) Frequency to cycle configuration and commandline in seconds"); +INSERT INTO `configuration` (field, value, description) VALUES("leaderboard_limit", "50", "(Integer) Maximum number of teams to show on the leaderboard"); INSERT INTO `configuration` (field, value, description) VALUES("registration", "0", "(Boolean) Ability to register teams"); INSERT INTO `configuration` (field, value, description) VALUES("registration_names", "0", "(Boolean) Registration will ask for names"); INSERT INTO `configuration` (field, value, description) VALUES("registration_type", "1", "(Integer) Type of registration: 1 - Open; 2 - Tokenized;"); INSERT INTO `configuration` (field, value, description) VALUES("registration_players", "3", "(Integer) Number of players per team"); +INSERT INTO `configuration` (field, value, description) VALUES("registration_facebook", "0", "(Boolean) Allow Facebook Registration"); +INSERT INTO `configuration` (field, value, description) VALUES("registration_google", "0", "(Boolean) Allow Google Registration"); +INSERT INTO `configuration` (field, value, description) VALUES("registration_prefix", "Hacker", "(String) Automated Team Registation Name Prefix"); INSERT INTO `configuration` (field, value, description) VALUES("ldap", "0", "(Boolean) Ability to use LDAP to login"); INSERT INTO `configuration` (field, value, description) VALUES("ldap_server", "ldap://localhost", "(String) LDAP Server"); INSERT INTO `configuration` (field, value, description) VALUES("ldap_port", "389", "(Integer) LDAP Port"); @@ -226,6 +254,8 @@ INSERT INTO `configuration` (field, value, description) VALUES("ldap_domain_suff INSERT INTO `configuration` (field, value, description) VALUES("login", "1", "(Boolean) Ability to login"); INSERT INTO `configuration` (field, value, description) VALUES("login_select", "0", "(Boolean) Login selecting the team"); INSERT INTO `configuration` (field, value, description) VALUES("login_strongpasswords", "0", "(Boolean) Enforce using strong passwords"); +INSERT INTO `configuration` (field, value, description) VALUES("login_facebook", "0", "(Boolean) Allow Facebook Login"); +INSERT INTO `configuration` (field, value, description) VALUES("login_google", "0", "(Boolean) Allow Google Login"); INSERT INTO `configuration` (field, value, description) VALUES("password_type", "1", "(Integer) Type of passwords: See table password_types"); INSERT INTO `configuration` (field, value, description) VALUES("default_bonus", "30", "(Integer) Default value for bonus in levels"); INSERT INTO `configuration` (field, value, description) VALUES("default_bonusdec", "10", "(Integer) Default bonus decrement in levels"); @@ -291,12 +321,13 @@ DROP TABLE IF EXISTS `registration_tokens`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `registration_tokens` ( `id` int(11) NOT NULL AUTO_INCREMENT, - `token` text NOT NULL, + `token` varchar(250) NOT NULL, `used` tinyint(1) NOT NULL, `team_id` int(11) NOT NULL, `created_ts` timestamp NOT NULL DEFAULT 0, `use_ts` timestamp NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `token` (`token`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -314,7 +345,9 @@ CREATE TABLE `scores_log` ( `points` int(11) NOT NULL, `level_id` int(11) NOT NULL, `type` varchar(4) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `level_id` (`level_id`), + KEY `team_id` (`team_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -331,7 +364,8 @@ CREATE TABLE `bases_log` ( `code` int(11) NOT NULL, `response` text NOT NULL, `level_id` int(11) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `level_id` (`level_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -344,12 +378,16 @@ DROP TABLE IF EXISTS `scripts`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `scripts` ( `id` int(11) NOT NULL AUTO_INCREMENT, + `host` varchar(1024) NOT NULL, `ts` timestamp NULL, `pid` int(11) NOT NULL, - `name` text NOT NULL, + `name` varchar(255) NOT NULL, `cmd` text NOT NULL, `status` tinyint(1) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `host` (`host`), + KEY `status` (`status`), + KEY `name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -366,7 +404,9 @@ CREATE TABLE `failures_log` ( `team_id` int(11) NOT NULL, `level_id` int(11) NOT NULL, `flag` text NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `team_id` (`team_id`), + KEY `level_id` (`level_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -383,7 +423,9 @@ CREATE TABLE `hints_log` ( `level_id` int(11) NOT NULL, `team_id` int(11) NOT NULL, `penalty` int(11) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `level_id` (`level_id`), + KEY `team_id` (`team_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/database/test_schema.sql b/database/test_schema.sql index 5beadecb..e91e2057 100644 --- a/database/test_schema.sql +++ b/database/test_schema.sql @@ -36,7 +36,7 @@ CREATE TABLE `levels` ( `id` int(11) NOT NULL AUTO_INCREMENT, `active` tinyint(1) NOT NULL, `type` varchar(4) NOT NULL, - `title` text NOT NULL, + `title` varchar(255) NOT NULL DEFAULT '', `description` text NOT NULL, `entity_id` int(11) NOT NULL, `category_id` int(11) NOT NULL, @@ -47,9 +47,11 @@ CREATE TABLE `levels` ( `flag` text NOT NULL, `hint` text NOT NULL, `penalty` int(11) NOT NULL, - `created_ts` timestamp NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; + `created_ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `entity_id` (`entity_id`), + KEY `active` (`active`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -61,7 +63,7 @@ DROP TABLE IF EXISTS `categories`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `categories` ( `id` int(11) NOT NULL AUTO_INCREMENT, - `category` text NOT NULL, + `category` varchar(255) NOT NULL, `protected` tinyint(1) NOT NULL, `created_ts` timestamp NOT NULL DEFAULT 0, PRIMARY KEY (`id`) @@ -116,8 +118,8 @@ DROP TABLE IF EXISTS `teams`; CREATE TABLE `teams` ( `id` int(11) NOT NULL AUTO_INCREMENT, `active` tinyint(1) NOT NULL DEFAULT 1, - `name` text NOT NULL, - `password_hash` text NOT NULL, + `name` varchar(255) NOT NULL, + `password_hash` varchar(255) NOT NULL, `points` int(11) NOT NULL DEFAULT 0, `last_score` timestamp NOT NULL, `logo` text NOT NULL, @@ -125,7 +127,9 @@ CREATE TABLE `teams` ( `protected` tinyint(1) NOT NULL DEFAULT 0, `visible` tinyint(1) NOT NULL DEFAULT 1, `created_ts` timestamp NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `visible` (`visible`), + KEY `active` (`active`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -146,6 +150,22 @@ CREATE TABLE `livesync` ( ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `teams_oauth` +-- + +DROP TABLE IF EXISTS `teams_oauth`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `teams_oauth` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `type` text NOT NULL, + `team_id` int(11) NOT NULL, + `token` text NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `teams_data` -- @@ -156,10 +176,11 @@ DROP TABLE IF EXISTS `teams_data`; CREATE TABLE `teams_data` ( `id` int(11) NOT NULL AUTO_INCREMENT, `team_id` int(11) NOT NULL, - `name` text NOT NULL, - `email` text NOT NULL, + `name` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, `created_ts` timestamp NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `team_id` (`team_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -172,14 +193,15 @@ DROP TABLE IF EXISTS `sessions`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `sessions` ( `id` int(11) NOT NULL AUTO_INCREMENT, - `cookie` text NOT NULL, + `cookie` varchar(200) NOT NULL, `data` text NOT NULL, `team_id` int(11) NOT NULL, `created_ts` timestamp NOT NULL DEFAULT 0, `last_access_ts` timestamp NOT NULL, - `last_page_access` text NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; + `last_page_access` varchar(200) NOT NULL, + PRIMARY KEY (`id`), + KEY `cookie` (`cookie`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -215,10 +237,16 @@ INSERT INTO `configuration` (field, value, description) VALUES("auto_announce", INSERT INTO `configuration` (field, value, description) VALUES("progressive_cycle", "300", "(Integer) Frequency to take progressive scoreboard in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("bases_cycle", "5", "(Integer) Frequency to score base levels in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("autorun_cycle", "30", "(Integer) Frequency to cycle autorun in seconds"); +INSERT INTO `configuration` (field, value, description) VALUES("gameboard_cycle", "5", "(Integer) Frequency to cycle gameboard in seconds"); +INSERT INTO `configuration` (field, value, description) VALUES("conf_cycle", "10", "(Integer) Frequency to cycle configuration and commandline in seconds"); +INSERT INTO `configuration` (field, value, description) VALUES("leaderboard_limit", "50", "(Integer) Maximum number of teams to show on the leaderboard"); INSERT INTO `configuration` (field, value, description) VALUES("registration", "0", "(Boolean) Ability to register teams"); INSERT INTO `configuration` (field, value, description) VALUES("registration_names", "0", "(Boolean) Registration will ask for names"); INSERT INTO `configuration` (field, value, description) VALUES("registration_type", "1", "(Integer) Type of registration: 1 - Open; 2 - Tokenized;"); INSERT INTO `configuration` (field, value, description) VALUES("registration_players", "3", "(Integer) Number of players per team"); +INSERT INTO `configuration` (field, value, description) VALUES("registration_facebook", "0", "(Boolean) Allow Facebook Registration"); +INSERT INTO `configuration` (field, value, description) VALUES("registration_google", "0", "(Boolean) Allow Google Registration"); +INSERT INTO `configuration` (field, value, description) VALUES("registration_prefix", "Hacker", "(String) Automated Team Registation Name Prefix"); INSERT INTO `configuration` (field, value, description) VALUES("ldap", "0", "(Boolean) Ability to use LDAP to login"); INSERT INTO `configuration` (field, value, description) VALUES("ldap_server", "ldap://localhost", "(String) LDAP Server"); INSERT INTO `configuration` (field, value, description) VALUES("ldap_port", "389", "(Integer) LDAP Port"); @@ -226,6 +254,8 @@ INSERT INTO `configuration` (field, value, description) VALUES("ldap_domain_suff INSERT INTO `configuration` (field, value, description) VALUES("login", "1", "(Boolean) Ability to login"); INSERT INTO `configuration` (field, value, description) VALUES("login_select", "0", "(Boolean) Login selecting the team"); INSERT INTO `configuration` (field, value, description) VALUES("login_strongpasswords", "0", "(Boolean) Enforce using strong passwords"); +INSERT INTO `configuration` (field, value, description) VALUES("login_facebook", "0", "(Boolean) Allow Facebook Login"); +INSERT INTO `configuration` (field, value, description) VALUES("login_google", "0", "(Boolean) Allow Google Login"); INSERT INTO `configuration` (field, value, description) VALUES("password_type", "1", "(Integer) Type of passwords: See table password_types"); INSERT INTO `configuration` (field, value, description) VALUES("default_bonus", "30", "(Integer) Default value for bonus in levels"); INSERT INTO `configuration` (field, value, description) VALUES("default_bonusdec", "10", "(Integer) Default bonus decrement in levels"); @@ -291,12 +321,13 @@ DROP TABLE IF EXISTS `registration_tokens`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `registration_tokens` ( `id` int(11) NOT NULL AUTO_INCREMENT, - `token` text NOT NULL, + `token` varchar(250) NOT NULL, `used` tinyint(1) NOT NULL, `team_id` int(11) NOT NULL, `created_ts` timestamp NOT NULL DEFAULT 0, `use_ts` timestamp NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `token` (`token`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -314,7 +345,9 @@ CREATE TABLE `scores_log` ( `points` int(11) NOT NULL, `level_id` int(11) NOT NULL, `type` varchar(4) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `level_id` (`level_id`), + KEY `team_id` (`team_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -331,7 +364,8 @@ CREATE TABLE `bases_log` ( `code` int(11) NOT NULL, `response` text NOT NULL, `level_id` int(11) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `level_id` (`level_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -344,12 +378,16 @@ DROP TABLE IF EXISTS `scripts`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `scripts` ( `id` int(11) NOT NULL AUTO_INCREMENT, + `host` varchar(1024) NOT NULL, `ts` timestamp NULL, `pid` int(11) NOT NULL, - `name` text NOT NULL, + `name` varchar(255) NOT NULL, `cmd` text NOT NULL, `status` tinyint(1) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `host` (`host`), + KEY `status` (`status`), + KEY `name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -366,7 +404,9 @@ CREATE TABLE `failures_log` ( `team_id` int(11) NOT NULL, `level_id` int(11) NOT NULL, `flag` text NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `team_id` (`team_id`), + KEY `level_id` (`level_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; @@ -383,7 +423,9 @@ CREATE TABLE `hints_log` ( `level_id` int(11) NOT NULL, `team_id` int(11) NOT NULL, `penalty` int(11) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `level_id` (`level_id`), + KEY `team_id` (`team_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/extra/nginx.conf b/extra/nginx.conf index cc305a47..c2a8b55a 100644 --- a/extra/nginx.conf +++ b/extra/nginx.conf @@ -40,6 +40,7 @@ server { location ~ \.php$ { try_files $uri =404; fastcgi_pass unix:/var/run/hhvm/sock; + fastcgi_intercept_errors on; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; diff --git a/extra/nginx/nginx.conf b/extra/nginx/nginx.conf index 61a8da02..c5edb099 100644 --- a/extra/nginx/nginx.conf +++ b/extra/nginx/nginx.conf @@ -39,6 +39,7 @@ server { index index.php; location /data/customlogos/ { + fastcgi_intercept_errors on; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_pass HHVMSERVER:9000; @@ -47,6 +48,7 @@ server { location ~ \.php$ { try_files $uri =404; fastcgi_pass HHVMSERVER:9000; + fastcgi_intercept_errors on; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; diff --git a/extra/settings.ini.example b/extra/settings.ini.example index 5b0cb752..3b65cd38 100644 --- a/extra/settings.ini.example +++ b/extra/settings.ini.example @@ -6,7 +6,9 @@ DB_NAME = 'DATABASE' DB_USERNAME = 'MYUSER' DB_PASSWORD = 'MYPWD' -MC_HOST = 'MCHOST' +MC_HOST[] = 'MCHOST' MC_PORT = '11211' +FACEBOOK_OAUTH_APP_ID = '' +FACEBOOK_OAUTH_APP_SECRET = '' GOOGLE_OAUTH_FILE = '' diff --git a/src/Db.php b/src/Db.php index c1a3bf28..7e101906 100644 --- a/src/Db.php +++ b/src/Db.php @@ -25,6 +25,11 @@ private function __construct() { private function __clone(): void {} + public static function getDatabaseStats(): array { + $db = self::getInstance(); + return $db->pool->getPoolStats(); + } + public function getBackupCmd(): string { $usr = must_have_idx($this->config, 'DB_USERNAME'); $pwd = must_have_idx($this->config, 'DB_PASSWORD'); diff --git a/src/Router.php b/src/Router.php index 22fad642..bffaf9a9 100644 --- a/src/Router.php +++ b/src/Router.php @@ -106,6 +106,6 @@ public static function isRequestModal(): bool { // Check to see if the request is going through the router public static function isRequestRouter(): bool { - return self::getRequestedPage() != "index"; + return self::getRequestedPage() !== "index"; } } diff --git a/src/SessionUtils.php b/src/SessionUtils.php index 0cadfa10..fe0d0301 100644 --- a/src/SessionUtils.php +++ b/src/SessionUtils.php @@ -110,14 +110,14 @@ public static function sessionActive(): bool { public static function enforceLogin(): void { /* HH_IGNORE_ERROR[2050] */ if (!self::sessionActive()) { - throw new IndexRedirectException(); + throw new LoginRedirectException(); } } public static function enforceAdmin(): void { /* HH_IGNORE_ERROR[2050] */ if (!array_key_exists('admin', $_SESSION)) { - throw new IndexRedirectException(); + throw new LoginRedirectException(); } } diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index 55cf8d4c..d8c002be 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -152,8 +152,10 @@ class="fb--conf--registration_type" // TODO: Translate password types private async function genStrongPasswordsSelect(): Awaitable<:xhp> { - $types = await Configuration::genAllPasswordTypes(); - $config = await Configuration::genCurrentPasswordType(); + list($types, $config) = await \HH\Asio\va( + Configuration::genAllPasswordTypes(), + Configuration::genCurrentPasswordType(), + ); $select = ; foreach ($types as $type) { $select->appendChild( @@ -170,10 +172,12 @@ class="fb--conf--password_type" } private async function genConfigurationDurationSelect(): Awaitable<:xhp> { - $config = await Configuration::gen('game_duration_unit'); - $duration_unit = $config->getValue(); - $config = await Configuration::gen('game_duration_value'); - $duration_value = $config->getValue(); + list($config_duration_unit, $config_duration_value) = await \HH\Asio\va( + Configuration::gen('game_duration_unit'), + Configuration::gen('game_duration_value'), + ); + $duration_unit = $config_duration_unit->getValue(); + $duration_value = $config_duration_value->getValue(); $minute_selected = $duration_unit === 'm'; $hour_selected = $duration_unit === 'h'; @@ -244,7 +248,7 @@ class="fb--conf--language" $tokens = await Token::genAllTokens(); foreach ($tokens as $token) { if ($token->getUsed()) { - $team = await MultiTeam::genTeam($token->getTeamId()); + $team = await MultiTeam::genTeam($token->getTeamId()); // TODO: Combine Awaits $token_status = {tr('Used by')} {$team->getName()} @@ -298,7 +302,12 @@ class="fb-cta cta--yellow" 'login' => Configuration::gen('login'), 'login_select' => Configuration::gen('login_select'), 'login_strongpasswords' => Configuration::gen('login_strongpasswords'), + 'login_facebook' => Configuration::gen('login_facebook'), + 'login_google' => Configuration::gen('login_google'), 'registration_names' => Configuration::gen('registration_names'), + 'registration_facebook' => Configuration::gen('registration_facebook'), + 'registration_google' => Configuration::gen('registration_google'), + 'registration_prefix' => Configuration::gen('registration_prefix'), 'ldap' => Configuration::gen('ldap'), 'ldap_server' => Configuration::gen('ldap_server'), 'ldap_port' => Configuration::gen('ldap_port'), @@ -310,6 +319,9 @@ class="fb-cta cta--yellow" 'progressive_cycle' => Configuration::gen('progressive_cycle'), 'default_bonus' => Configuration::gen('default_bonus'), 'default_bonusdec' => Configuration::gen('default_bonusdec'), + 'gameboard_cycle' => Configuration::gen('gameboard_cycle'), + 'conf_cycle' => Configuration::gen('conf_cycle'), + 'leaderboard_limit' => Configuration::gen('leaderboard_limit'), 'bases_cycle' => Configuration::gen('bases_cycle'), 'autorun_cycle' => Configuration::gen('autorun_cycle'), 'start_ts' => Configuration::gen('start_ts'), @@ -330,7 +342,13 @@ class="fb-cta cta--yellow" $login = $results['login']; $login_select = $results['login_select']; $login_strongpasswords = $results['login_strongpasswords']; + $login_facebook = $results['login_facebook']; + $login_google = $results['login_google']; $registration_names = $results['registration_names']; + $registration_facebook = $results['registration_facebook']; + $registration_google = $results['registration_google']; + $registration_prefix = $results['registration_prefix']; + $login_google = $results['login_google']; $ldap = $results['ldap']; $ldap_server = $results['ldap_server']; $ldap_port = $results['ldap_port']; @@ -342,6 +360,9 @@ class="fb-cta cta--yellow" $progressive_cycle = $results['progressive_cycle']; $default_bonus = $results['default_bonus']; $default_bonusdec = $results['default_bonusdec']; + $gameboard_cycle = $results['gameboard_cycle']; + $conf_cycle = $results['conf_cycle']; + $leaderboard_limit = $results['leaderboard_limit']; $bases_cycle = $results['bases_cycle']; $autorun_cycle = $results['autorun_cycle']; $start_ts = $results['start_ts']; @@ -352,13 +373,20 @@ class="fb-cta cta--yellow" $custom_org = $results['custom_org']; $custom_byline = $results['custom_byline']; $custom_logo_image = $results['custom_logo_image']; - $registration_on = $registration->getValue() === '1'; $registration_off = $registration->getValue() === '0'; $login_on = $login->getValue() === '1'; $login_off = $login->getValue() === '0'; $login_select_on = $login_select->getValue() === '1'; $login_select_off = $login_select->getValue() === '0'; + $login_facebook_on = $login_facebook->getValue() === '1'; + $login_facebook_off = $login_facebook->getValue() === '0'; + $login_google_on = $login_google->getValue() === '1'; + $login_google_off = $login_google->getValue() === '0'; + $registration_facebook_on = $registration_facebook->getValue() === '1'; + $registration_facebook_off = $registration_facebook->getValue() === '0'; + $registration_google_on = $registration_google->getValue() === '1'; + $registration_google_off = $registration_google->getValue() === '0'; $ldap_on = $ldap->getValue() === '1'; $ldap_off = $ldap->getValue() === '0'; $strong_passwords_on = $login_strongpasswords->getValue() === '1'; @@ -381,7 +409,12 @@ class="fb-cta cta--yellow" $game_start_array = array(); if ($start_ts->getValue() !== '0' && $start_ts->getValue() !== 'NaN') { $game_start_ts = $start_ts->getValue(); - $game_start_array = getdate($game_start_ts); + $game_start_array = array(); + $game_start_array['year'] = gmdate('Y', $game_start_ts); + $game_start_array['mon'] = gmdate('m', $game_start_ts); + $game_start_array['mday'] = gmdate('d', $game_start_ts); + $game_start_array['hours'] = gmdate('H', $game_start_ts); + $game_start_array['minutes'] = gmdate('i', $game_start_ts); } else { $game_start_ts = '0'; $game_start_array['year'] = '0'; @@ -394,7 +427,12 @@ class="fb-cta cta--yellow" $game_end_array = array(); if ($end_ts->getValue() !== '0' && $end_ts->getValue() !== 'NaN') { $game_end_ts = $end_ts->getValue(); - $game_end_array = getdate($game_end_ts); + $game_end_array = array(); + $game_end_array['year'] = gmdate('Y', $game_end_ts); + $game_end_array['mon'] = gmdate('m', $game_end_ts); + $game_end_array['mday'] = gmdate('d', $game_end_ts); + $game_end_array['hours'] = gmdate('H', $game_end_ts); + $game_end_array['minutes'] = gmdate('i', $game_end_ts); } else { $game_end_ts = '0'; $game_end_array['year'] = '0'; @@ -407,10 +445,16 @@ class="fb-cta cta--yellow" if ($game->getValue() === '0') { $timer_start_ts = tr('Not started yet'); $timer_end_ts = tr('Not started yet'); + $game_schedule_reset_text = tr('Reset Schedule'); + $game_schedule_reset_class = 'fb-cta cta--red'; + $game_schedule_reset_action = 'reset-game-schedule'; } else { $timer_start_ts = date(tr('date and time format'), $start_ts->getValue()); $timer_end_ts = date(tr('date and time format'), $end_ts->getValue()); + $game_schedule_reset_text = tr('Game Running'); + $game_schedule_reset_class = 'fb-cta cta--yellowe'; + $game_schedule_reset_action = ''; } $registration_type = await Configuration::gen('registration_type'); @@ -655,6 +699,119 @@ class="icon--badge" +
+
+

{tr('Integration')}

+
+
+
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+
+
+ + getValue()} + name="fb--conf--registration_prefix" + /> +
+
+
+

{tr('Active Directory / LDAP')}

@@ -745,6 +902,14 @@ class="icon--badge" name="fb--conf--progressive_cycle" /> +
+ + getValue()} + name="fb--conf--bases_cycle" + /> +
@@ -778,34 +943,16 @@ class="icon--badge" name="fb--conf--default_bonus" />
-
-
-
- - getValue()} - name="fb--conf--bases_cycle" - /> -
- + getValue()} - name="fb--conf--default_bonusdec" + value={$gameboard_cycle->getValue()} + name="fb--conf--gameboard_cycle" />
-
-
- - getValue()} - name="fb--conf--autorun_cycle" - /> -
+
@@ -829,6 +976,40 @@ class="icon--badge"
+
+ + getValue()} + name="fb--conf--default_bonusdec" + /> +
+
+ + getValue()} + name="fb--conf--conf_cycle" + /> +
+
+
+
+ + getValue()} + name="fb--conf--autorun_cycle" + /> +
+
+ + getValue()} + name="fb--conf--leaderboard_limit" + /> +
@@ -836,6 +1017,13 @@ class="icon--badge"

{tr('Game Schedule')}

+
+ +
@@ -1597,7 +1785,7 @@ class= $quiz_id_txt = 'quiz_id'.strval($quiz->getId()); $countries_select = - await $this->genGenerateCountriesSelect($quiz->getEntityId()); + await $this->genGenerateCountriesSelect($quiz->getEntityId()); // TODO: Combine Awaits $delete_button =
@@ -1771,20 +1959,15 @@ class= } public async function genRenderFlagsContent(): Awaitable<:xhp> { - $awaitables = Map { - 'countries_select' => $this->genGenerateCountriesSelect(0), - 'level_categories_select' => $this->genGenerateLevelCategoriesSelect( - 0, - ), - 'filter_categories_select' => - $this->genGenerateFilterCategoriesSelect(), - }; - - $results = await \HH\Asio\m($awaitables); - - $countries_select = $results['countries_select']; - $level_categories_select = $results['level_categories_select']; - $filter_categories_select = $results['filter_categories_select']; + list( + $countries_select, + $level_categories_select, + $filter_categories_select, + ) = await \HH\Asio\va( + $this->genGenerateCountriesSelect(0), + $this->genGenerateLevelCategoriesSelect(0), + $this->genGenerateFilterCategoriesSelect(), + ); $adminsections =
@@ -1990,11 +2173,11 @@ class="fb-cta cta--yellow"
; - $attachments = await Attachment::genHasAttachments($flag->getId()); + $attachments = await Attachment::genHasAttachments($flag->getId()); // TODO: Combine Awaits if ($attachments) { $a_c = 1; $all_attachments = - await Attachment::genAllAttachments($flag->getId()); + await Attachment::genAllAttachments($flag->getId()); // TODO: Combine Awaits foreach ($all_attachments as $attachment) { $attachments_div->appendChild(
@@ -2072,10 +2255,10 @@ class="fb-cta cta--red"
; - $links = await Link::genHasLinks($flag->getId()); + $links = await Link::genHasLinks($flag->getId()); // TODO: Combine Awaits if ($links) { $l_c = 1; - $all_links = await Link::genAllLinks($flag->getId()); + $all_links = await Link::genAllLinks($flag->getId()); // TODO: Combine Awaits foreach ($all_links as $link) { $links_div->appendChild(
; - $has_attachments = await Attachment::genHasAttachments($base->getId()); + $has_attachments = await Attachment::genHasAttachments($base->getId()); // TODO: Combine Awaits if ($has_attachments) { $a_c = 1; $all_attachments = - await Attachment::genAllAttachments($base->getId()); + await Attachment::genAllAttachments($base->getId()); // TODO: Combine Awaits foreach ($all_attachments as $attachment) { $attachments_div->appendChild(
@@ -2605,10 +2775,10 @@ class="fb-cta cta--red"
; - $has_links = await Link::genHasLinks($base->getId()); + $has_links = await Link::genHasLinks($base->getId()); // TODO: Combine Awaits if ($has_links) { $l_c = 1; - $all_links = await Link::genAllLinks($base->getId()); + $all_links = await Link::genAllLinks($base->getId()); // TODO: Combine Awaits foreach ($all_links as $link) { if (filter_var($link->getLink(), FILTER_VALIDATE_URL)) { $link_a = @@ -2652,18 +2822,10 @@ class="fb-cta cta--red" $l_c++; } - $awaitables = Map { - 'countries_select' => $this->genGenerateCountriesSelect( - $base->getEntityId(), - ), - 'level_categories_select' => - $this->genGenerateLevelCategoriesSelect($base->getCategoryId()), - }; - - $results = await HH\Asio\m($awaitables); - - $countries_select = $results['countries_select']; - $level_categories_select = $results['level_categories_select']; + list($countries_select, $level_categories_select) = await \HH\Asio\va( + $this->genGenerateCountriesSelect($base->getEntityId()), + $this->genGenerateLevelCategoriesSelect($base->getCategoryId()), + ); // TODO: Combine Awaits $adminsections->appendChild(
@@ -2870,7 +3032,7 @@ class="highlighted--yellow" ; } - $is_used = await Category::genIsUsed($category->getId()); + $is_used = await Category::genIsUsed($category->getId()); // TODO: Combine Awaits ; if ($is_used || $category->getProtected()) { $delete_action = ; @@ -2968,7 +3130,7 @@ class="not_configuration" $all_countries = await Country::genAllCountries(); foreach ($all_countries as $country) { - $using_country = await Level::genWhoUses($country->getId()); + $using_country = await Level::genWhoUses($country->getId()); // TODO: Combine Awaits $current_use = ($using_country) ? tr('Yes') : tr('No'); if ($country->getEnabled()) { $highlighted_action = 'disable_country'; @@ -3092,12 +3254,12 @@ class={$highlighted_color} private async function genGenerateTeamScores(int $team_id): Awaitable<:xhp> { $scores_div =
; - $scores = await ScoreLog::genAllScoresByTeam($team_id); + $scores = await ScoreLog::genAllScoresByTeam($team_id, true); if (count($scores) > 0) { $scores_tbody = ; foreach ($scores as $score) { - $level = await Level::gen($score->getLevelId()); - $country = await Country::gen($level->getEntityId()); + $level = await Level::gen($score->getLevelId()); // TODO: Combine Awaits + $country = await Country::gen($level->getEntityId()); // TODO: Combine Awaits $level_str = $country->getName().' - '.$level->getTitle(); $scores_tbody->appendChild( @@ -3142,7 +3304,7 @@ class={$highlighted_color} if (count($failures) > 0) { $failures_tbody = ; foreach ($failures as $failure) { - $check_status = await Level::genCheckStatus($failure->getLevelId()); + $check_status = await Level::genCheckStatus($failure->getLevelId()); // TODO: Combine Awaits if (!$check_status) { continue; } @@ -3340,7 +3502,7 @@ class="fb-cta cta--yellow js-confirm-save" $c = 1; $all_teams = await Team::genAllTeams(); foreach ($all_teams as $team) { - $logo_model = await $team->getLogoModel(); + $logo_model = await $team->getLogoModel(); // TODO: Combine Awaits if ($logo_model->getCustom()) { $image = getLogo()}>; } else { @@ -3461,7 +3623,7 @@ class="fb-cta cta--red js-delete-team" 'team_failures' => $this->genGenerateTeamFailures($team->getId()), }; - $results = await \HH\Asio\m($awaitables); + $results = await \HH\Asio\m($awaitables); // TODO: Combine Awaits $team_tabs = $results['team_tabs']; $team_names = $results['team_names']; @@ -3654,7 +3816,7 @@ class="fb-cta cta--yellow js-confirm-save" ; } - $using_logo = await MultiTeam::genWhoUses($logo->getName()); + $using_logo = await MultiTeam::genWhoUses($logo->getName()); // TODO: Combine Awaits $current_use = (count($using_logo) > 0) ? tr('Yes') : tr('No'); if ($logo->getEnabled()) { $highlighted_action = 'disable_logo'; @@ -3745,8 +3907,17 @@ class={$highlighted_color} $c = 1; $all_sessions = await Session::genAllSessions(); foreach ($all_sessions as $session) { + /* HH_IGNORE_ERROR[2050] */ + $cookie = $_COOKIE['FBCTF']; + if ($cookie === $session->getCookie()) { + $session_data = await Session::genSessionDataIfExist($cookie); // TODO: Combine Awaits + await Session::genSetTeamId($cookie, $session_data); // TODO: Combine Awaits + $session = await Session::gen($cookie); // TODO: Combine Awaits + } else if ($session->getTeamId() === 0) { + continue; + } $session_id = 'session_'.strval($session->getId()); - $team = await MultiTeam::genTeam($session->getTeamId()); + $team = await MultiTeam::genTeam($session->getTeamId()); // TODO: Combine Awaits $adminsections->appendChild(
@@ -3859,43 +4030,37 @@ class={$highlighted_color} {$gamelog->getEntry()}; } - $awaitables = Map { - 'team' => MultiTeam::genTeam($gamelog->getTeamId()), - 'level' => Level::gen($gamelog->getLevelId()), - }; - - $results = await \HH\Asio\m($awaitables); + list($team, $level) = await \HH\Asio\va( + MultiTeam::genTeam($gamelog->getTeamId()), + Level::gen($gamelog->getLevelId()), + ); // TODO: Combine Awaits - if ($results->contains('team') && $results->contains('level')) { - $team = $results->get('team'); - invariant($team !== null, 'Team should not be null'); - invariant($team instanceof Team, 'team should be of type Team'); + invariant($team !== null, 'Team should not be null'); + invariant($team instanceof Team, 'team should be of type Team'); - $level = $results->get('level'); - invariant($level !== null, 'Level should not be null'); - invariant($level instanceof Level, 'level should be of type Level'); + invariant($level !== null, 'Level should not be null'); + invariant($level instanceof Level, 'level should be of type Level'); - $country = await Country::gen($level->getEntityId()); + $country = await Country::gen($level->getEntityId()); // TODO: Combine Awaits - $team_name = $team->getName(); + $team_name = $team->getName(); - $level_str = - $country->getName(). - ' - '. - $level->getTitle(). - ' - '. - $level->getType(); - $logs_tbody->appendChild( - - {time_ago($gamelog->getTs())} - {$log_entry} - {$level_str} - {strval($gamelog->getPoints())} - {$team_name} - {$gamelog->getFlag()} - - ); - } + $level_str = + $country->getName(). + ' - '. + $level->getTitle(). + ' - '. + $level->getType(); + $logs_tbody->appendChild( + + {time_ago($gamelog->getTs())} + {$log_entry} + {$level_str} + {strval($gamelog->getPoints())} + {$team_name} + {$gamelog->getFlag()} + + ); $logs_table = @@ -4038,10 +4203,11 @@ public function renderMainContent(): :xhp { {tr('Game Logs')} + +
{$game_action} -


{$pause_action} - +
diff --git a/src/data/announcements.php b/src/data/announcements.php index 02b4aec0..7e6cbf21 100644 --- a/src/data/announcements.php +++ b/src/data/announcements.php @@ -2,12 +2,13 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - class AnnouncementsDataController extends DataController { public async function genGenerateData(): Awaitable { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + $data = array(); $all_announcements = await Announcement::genAllAnnouncements(); @@ -19,5 +20,6 @@ class AnnouncementsDataController extends DataController { } } +/* HH_IGNORE_ERROR[1002] */ $announcementsData = new AnnouncementsDataController(); -\HH\Asio\join($announcementsData->genGenerateData()); +$announcementsData->sendData(); diff --git a/src/data/attachment.php b/src/data/attachment.php index 811d9493..c7a3d5b7 100644 --- a/src/data/attachment.php +++ b/src/data/attachment.php @@ -29,13 +29,10 @@ class AttachmentDataController extends DataController { } } - header('Content-Type: application/octet-stream'); - header("Content-Transfer-Encoding: Binary"); - header('Content-disposition: attachment; filename="'.$filename.'"'); - print $data; + $this->downloadSend($filename, $data); } } /* HH_IGNORE_ERROR[1002] */ -$attachment_file = new AttachmentDataController(); -\HH\Asio\join($attachment_file->genGenerateData()); +$attachmentData = new AttachmentDataController(); +$attachmentData->sendData(); diff --git a/src/data/captures.php b/src/data/captures.php new file mode 100644 index 00000000..b30472f4 --- /dev/null +++ b/src/data/captures.php @@ -0,0 +1,28 @@ + { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + + $data = array(); + + $my_team_id = SessionUtils::sessionTeam(); + + $captures = await ScoreLog::genAllScoresByTeam($my_team_id); + + foreach ($captures as $capture) { + $data[] = $capture->getLevelId(); + } + + $this->jsonSend($data); + } +} + +/* HH_IGNORE_ERROR[1002] */ +$capturesData = new CapturesController(); +$capturesData->sendData(); diff --git a/src/data/command-line.php b/src/data/command-line.php index c1b8932f..c3644e73 100644 --- a/src/data/command-line.php +++ b/src/data/command-line.php @@ -2,12 +2,13 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - class CommandsController extends DataController { public async function genGenerateData(): Awaitable { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + // Object to hold all the data. $commands_line_data = (object) array(); @@ -15,7 +16,18 @@ class CommandsController extends DataController { $results_library = (object) array(); $results_library_key = "results_library"; - $all_levels = await Level::genAllLevels(); + list( + $all_levels, + $all_enabled_countries, + $all_visible_teams, + $all_categories, + ) = await \HH\Asio\va( + Level::genAllLevels(), + Country::genAllEnabledCountries(), + MultiTeam::genAllVisibleTeams(), + Category::genAllCategories(), + ); + $levels_map = Map {}; foreach ($all_levels as $level) { $levels_map[$level->getEntityId()] = $level; @@ -24,7 +36,6 @@ class CommandsController extends DataController { // List of active countries. $countries_results = array(); $countries_key = "country_list"; - $all_enabled_countries = await Country::genAllEnabledCountries(); foreach ($all_enabled_countries as $country) { $level = $levels_map->get($country->getId()); $is_active_level = $level !== null && $level->getActive(); @@ -48,7 +59,6 @@ class CommandsController extends DataController { // List of active teams. $teams_results = array(); $teams_key = "teams"; - $all_visible_teams = await MultiTeam::genAllVisibleTeams(); foreach ($all_visible_teams as $team) { array_push($teams_results, $team->getName()); } @@ -56,7 +66,6 @@ class CommandsController extends DataController { // List of level categories. $categories_results = array(); $categories_key = "categories"; - $all_categories = await Category::genAllCategories(); foreach ($all_categories as $category) { array_push($categories_results, $category->getCategory()); } @@ -128,4 +137,4 @@ class CommandsController extends DataController { } $cmd = new CommandsController(); -\HH\Asio\join($cmd->genGenerateData()); +$cmd->sendData(); diff --git a/src/data/configuration.php b/src/data/configuration.php index 4522cef8..6f7a71a4 100644 --- a/src/data/configuration.php +++ b/src/data/configuration.php @@ -2,35 +2,41 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - class ConfigurationController extends DataController { - // Refresh rate for teams/leaderboard in milliseconds - private string $teams_cycle = "5000"; - // Refresh rate for map/announcements in milliseconds - private string $map_cycle = "5000"; - // Refresh rate for configuration values in milliseconds - private string $conf_cycle = "10000"; - // Refresh rate for commands in milliseconds - private string $cmd_cycle = "10000"; public async function genGenerateData(): Awaitable { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + $conf_data = (object) array(); $control = new Control(); - $gameboard = await Configuration::gen('gameboard'); + $awaitables = Map { + 'gameboard' => Configuration::gen('gameboard'), + 'gameboard_cycle' => Configuration::gen('gameboard_cycle'), + 'conf_cycle' => Configuration::gen('conf_cycle'), + }; + $awaitables_results = await \HH\Asio\m($awaitables); + + $gameboard = $awaitables_results['gameboard']; + // Refresh rate for teams/leaderboard in milliseconds + // Refresh rate for map/announcements in milliseconds + $gameboard_cycle = $awaitables_results['gameboard_cycle']; + // Refresh rate for configuration values in milliseconds + // Refresh rate for commands in milliseconds + $conf_cycle = $awaitables_results['conf_cycle']; /* HH_FIXME[1002] */ /* HH_FIXME[2011] */ $conf_data->{'currentTeam'} = SessionUtils::sessionTeamName(); $conf_data->{'gameboard'} = $gameboard->getValue(); - $conf_data->{'refreshTeams'} = $this->teams_cycle; - $conf_data->{'refreshMap'} = $this->map_cycle; - $conf_data->{'refreshConf'} = $this->conf_cycle; - $conf_data->{'refreshCmd'} = $this->cmd_cycle; + $conf_data->{'refreshTeams'} = ($gameboard_cycle->getValue()) * 1000; + $conf_data->{'refreshMap'} = ($gameboard_cycle->getValue()) * 1000; + $conf_data->{'refreshConf'} = ($conf_cycle->getValue()) * 1000; + $conf_data->{'refreshCmd'} = ($conf_cycle->getValue()) * 1000; $conf_data->{'progressiveCount'} = await Progressive::genCount(); $this->jsonSend($conf_data); @@ -38,4 +44,4 @@ class ConfigurationController extends DataController { } $confController = new ConfigurationController(); -\HH\Asio\join($confController->genGenerateData()); +$confController->sendData(); diff --git a/src/data/controller.php b/src/data/controller.php index 278684cc..9c7f8aa4 100644 --- a/src/data/controller.php +++ b/src/data/controller.php @@ -4,8 +4,31 @@ abstract class DataController { abstract public function genGenerateData(): Awaitable; + public function sendData(): void { + try { + \HH\Asio\join($this->genGenerateData()); + } catch (RedirectException $e) { + if (get_class($this) === "SessionController") { + error_log( + 'RedirectException: ('.get_class($e).') '.$e->getTraceAsString(), + ); + http_response_code($e->getStatusCode()); + Utils::redirect($e->getPath()); + } else { + $this->jsonSend(array()); + } + } + } + public function jsonSend(mixed $data): void { header('Content-Type: application/json'); print json_encode($data); } + + public function downloadSend(string $name, mixed $data): void { + header('Content-Type: application/octet-stream'); + header("Content-Transfer-Encoding: Binary"); + header('Content-disposition: attachment; filename="'.$name.'"'); + print $data; + } } diff --git a/src/data/country-data.php b/src/data/country-data.php index e3e2a632..204fbe36 100644 --- a/src/data/country-data.php +++ b/src/data/country-data.php @@ -2,47 +2,79 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - class CountryDataController extends DataController { public async function genGenerateData(): Awaitable { - $my_team = await MultiTeam::genTeam(SessionUtils::sessionTeam()); + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + + list($my_team, $gameboard, $all_active_levels) = await \HH\Asio\va( + MultiTeam::genTeam(SessionUtils::sessionTeam()), + Configuration::gen('gameboard'), + Level::genAllActiveLevels(), + ); $countries_data = (object) array(); // If gameboard refresing is disabled, exit - $gameboard = await Configuration::gen('gameboard'); if ($gameboard->getValue() === '0') { $this->jsonSend($countries_data); exit(1); } - $all_active_levels = await Level::genAllActiveLevels(); foreach ($all_active_levels as $level) { - $country = await Country::gen(intval($level->getEntityId())); + $awaitables = Map { + 'country' => Country::gen(intval($level->getEntityId())), + 'category' => Category::genSingleCategory($level->getCategoryId()), + 'attachments_list' => Attachment::genAllAttachmentsFileNamesLinks( + $level->getId(), + ), + 'links_list' => Link::genAllLinksValues($level->getId()), + 'completed_by' => MultiTeam::genCompletedLevelTeamNames( + $level->getId(), + ), + }; + $awaitables_results = await \HH\Asio\m($awaitables); // TODO: Combine Awaits + + $country = $awaitables_results['country']; + $category = $awaitables_results['category']; + $attachments_list = $awaitables_results['attachments_list']; + $links_list = $awaitables_results['links_list']; + $completed_by = $awaitables_results['completed_by']; + + invariant( + $country instanceof Country, + 'country should be of type Country', + ); + invariant( + $category instanceof Category, + 'category should be of type Category', + ); + if (!$country) { continue; } - $category = await Category::genSingleCategory($level->getCategoryId()); if ($level->getHint() !== '') { // There is hint, can this team afford it? if ($level->getPenalty() > $my_team->getPoints()) { // Not enough points $hint_cost = -2; $hint = 'no'; } else { - $hint = await HintLog::genPreviousHint( - $level->getId(), - $my_team->getId(), - false, - ); - $score = await ScoreLog::genPreviousScore( - $level->getId(), - $my_team->getId(), - false, - ); + list($hint, $score) = await \HH\Asio\va( + HintLog::genPreviousHint( + $level->getId(), + $my_team->getId(), + false, + ), + ScoreLog::genPreviousScore( + $level->getId(), + $my_team->getId(), + false, + ), + ); // TODO: Combine Awaits + // Has this team requested this hint or scored this level before? if ($hint || $score) { $hint_cost = 0; @@ -56,40 +88,9 @@ class CountryDataController extends DataController { $hint = 'no'; } - // All attachments for this level - $attachments_list = array(); - $has_attachments = await Attachment::genHasAttachments($level->getId()); - if ($has_attachments) { - $all_attachments = - await Attachment::genAllAttachments($level->getId()); - foreach ($all_attachments as $attachment) { - $attachment_details = array(); - $attachment_details['filename'] = $attachment->getFilename(); - $attachment_details['file_link'] = $attachment->getFileLink(); - array_push($attachments_list, $attachment_details); - } - } - - // All links for this level - $links_list = array(); - $has_links = await Link::genHasLinks($level->getId()); - if ($has_links) { - $all_links = await Link::genAllLinks($level->getId()); - foreach ($all_links as $link) { - array_push($links_list, $link->getLink()); - } - } - - // All teams that have completed this level - $completed_by = array(); - $completed_level = await MultiTeam::genCompletedLevel($level->getId()); - foreach ($completed_level as $c) { - array_push($completed_by, $c->getName()); - } - // Who is the first owner of this level - if ($completed_level) { - $owner = await MultiTeam::genFirstCapture($level->getId()); + if ($completed_by) { + $owner = await MultiTeam::genFirstCapture($level->getId()); // TODO: Combine Awaits $owner = $owner->getName(); } else { $owner = 'Uncaptured'; @@ -119,4 +120,4 @@ class CountryDataController extends DataController { } $countryData = new CountryDataController(); -\HH\Asio\join($countryData->genGenerateData()); +$countryData->sendData(); diff --git a/src/data/google_oauth.php b/src/data/google_oauth.php deleted file mode 100644 index 959ff0b1..00000000 --- a/src/data/google_oauth.php +++ /dev/null @@ -1,128 +0,0 @@ -setAuthConfig($google_oauth_file); - $client->setAccessType('offline'); - $client->setScopes(['profile email']); - $client->setRedirectUri( - 'https://'.$_SERVER['HTTP_HOST'].'/data/google_oauth.php', - ); - - $integration_csrf_token = base64_encode(random_bytes(100)); - // Cookie is sent with headers, and therefore not set until after the PHP code executes - this allows us to reset the cookie on each request without clobbering the state - setcookie( - 'integration_csrf_token', - strval($integration_csrf_token), - 0, - '/data/', - must_have_string(Utils::getSERVER(), 'SERVER_NAME'), - true, - true, - ); - $client->setState(strval($integration_csrf_token)); - - if ($code !== false) { - $integration_csrf_token = /* HH_IGNORE_ERROR[2050] */ - idx($_COOKIE, 'integration_csrf_token', false); - if (strval($integration_csrf_token) === '' || - strval($state) === '' || - strval($integration_csrf_token) != strval($state)) { - $code = false; - $error = false; - } - } - - if ($code !== false) { - $client->authenticate($code); - $access_token = $client->getAccessToken(); - $oauth_client = new Google_Service_Oauth2($client); - $profile = $oauth_client->userinfo->get(); - $livesync_password_update = \HH\Asio\join( - Team::genSetLiveSyncPassword( - SessionUtils::sessionTeam(), - "google_oauth", - $profile->email, - $profile->id, - ), - ); - if ($livesync_password_update === true) { - $message = - tr('Your FBCTF account was successfully linked with Google.'); - $javascript_status = - 'window.opener.document.getElementsByClassName("google-link-response")[0].innerHTML = "'. - tr('Your FBCTF account was successfully linked with Google.'). - '"'; - } else { - $message = - tr( - 'There was an error connecting your account to Google, please try again later.', - ); - $javascript_status = - 'window.opener.document.getElementsByClassName("google-link-response")[0].innerHTML = "'. - tr( - 'There was an error connecting your account to Google, please try again later.', - ). - '"'; - } - $javascript_close = "window.open('', '_self', ''); window.close();"; - } else if ($error === true) { - $message = - tr( - 'There was an error connecting your account to Google, please try again later.', - ); - $javascript_status = - 'window.opener.document.getElementsByClassName("google-link-response")[0].innerHTML = "'. - tr( - 'There was an error connecting your account to Google, please try again later.', - ). - '"'; - $javascript_close = "window.open('', '_self', ''); window.close();"; - } else { - $auth_url = $client->createAuthUrl(); - header('Location: '.filter_var($auth_url, FILTER_SANITIZE_URL)); - exit; - } -} else { - $message = tr('Google OAuth is disabled.'); - $javascript_status = - 'window.opener.document.getElementsByClassName("google-link-response")[0].innerHTML = "'. - tr('Google OAuth is disabled.'). - '"'; - $javascript_close = "window.open('', '_self', ''); window.close();"; -} - -$output = -
- - - - {$message} -
- - - -
; - -print $output; diff --git a/src/data/integration_login.php b/src/data/integration_login.php new file mode 100644 index 00000000..57b923e6 --- /dev/null +++ b/src/data/integration_login.php @@ -0,0 +1,57 @@ + { + $type = idx(Utils::getGET(), 'type'); + + if (!is_string($type)) { + $type = "none"; + } + + switch ($type) { + case "facebook": + await self::genProcessFacebookLogin(); + break; + case "google": + await self::genProcessGoogleLogin(); + break; + // FALLTHROUGH + default: + header('Location: /index.php?page=login'); + exit; + break; + } + } + + public static async function genProcessFacebookLogin(): Awaitable { + $enabled = await Integration::facebookLoginEnabled(); + if ($enabled === true) { + $url = await Integration::genFacebookLogin(); + header('Location: '.filter_var($url, FILTER_SANITIZE_URL)); + exit; + } else { + header('Location: /index.php?page=login'); + exit; + } + } + + public static async function genProcessGoogleLogin(): Awaitable { + $enabled = await Integration::googleLoginEnabled(); + if ($enabled === true) { + $url = await Integration::genGoogleLogin(); + header('Location: '.filter_var($url, FILTER_SANITIZE_URL)); + exit; + } else { + header('Location: /index.php?page=login'); + exit; + } + } +} + +$integration_login = new IntegrationLogin(); +\HH\Asio\join($integration_login->genProcessLogin()); diff --git a/src/data/integration_oauth.php b/src/data/integration_oauth.php new file mode 100644 index 00000000..245f7cb8 --- /dev/null +++ b/src/data/integration_oauth.php @@ -0,0 +1,143 @@ + { + + try { + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + } catch (RedirectException $e) { + error_log( + 'RedirectException: ('.get_class($e).') '.$e->getTraceAsString(), + ); + http_response_code($e->getStatusCode()); + Utils::redirect($e->getPath()); + } + + $type = idx(Utils::getGET(), 'type'); + + if (!is_string($type)) { + $type = "none"; + } + + $status = false; + + switch ($type) { + case "facebook": + $status = await self::genProcessFacebookOAuth(); + $provider = "Facebook"; + $container = "facebook-link-response"; + $button = "facebook-oauth-button"; + break; + case "google": + $status = await self::genProcessGoogleOAuth(); + $provider = "Google"; + $container = "google-link-response"; + $button = "google-oauth-button"; + break; + // FALLTHROUGH + default: + $provider = ''; + $container = ''; + $button = ''; + break; + } + + await self::genOutput($status, $provider, $container, $button); + } + + public static async function genOutput( + bool $status, + string $provider, + string $container, + string $button, + ): Awaitable { + await tr_start(); + if ($status === true) { //facebook-link-response + $message = + tr('Your FBCTF account was successfully linked with '.$provider.'.'); + $javascript_status = + 'window.opener.document.getElementsByClassName("'. + $container. + '")[0].innerHTML = "'. + tr('Your FBCTF account was successfully linked with '.$provider.'.'). + '"'; + $javascript_button = + 'window.opener.document.getElementsByName("'. + $button. + '")[0].style.backgroundColor="#1f7a1f"'; + } else { + $message = tr( + 'There was an error connecting your account to '. + $provider. + ', please try again later.', + ); + $javascript_status = + 'window.opener.document.getElementsByClassName("'. + $container. + '")[0].innerHTML = "'. + tr( + 'There was an error connecting your account to '. + $provider. + ', please try again later.', + ). + '"'; + $javascript_button = + 'window.opener.document.getElementsByName("'. + $button. + '")[0].style.backgroundColor="#800000"'; + } + + $javascript_close = "window.open('', '_self', ''); window.close();"; + + $output = +
+ + + + + {$message} +
+ + + +
; + + print $output; + } + + public static async function genProcessFacebookOAuth(): Awaitable { + $enabled = await Integration::facebookOAuthEnabled(); + if ($enabled === true) { + $status = await Integration::genFacebookOAuth(); + return $status; + } + return false; + } + + public static async function genProcessGoogleOAuth(): Awaitable { + $enabled = await Integration::googleOAuthEnabled(); + if ($enabled === true) { + $status = await Integration::genGoogleOAuth(); + return $status; + } + return false; + } + +} + +/* HH_IGNORE_ERROR[1002] */ +$integration_oauth = new IntegrationOAuth(); +\HH\Asio\join($integration_oauth->genProcessOAuth()); diff --git a/src/data/leaderboard.php b/src/data/leaderboard.php index f75e4e74..81cf3645 100644 --- a/src/data/leaderboard.php +++ b/src/data/leaderboard.php @@ -2,12 +2,13 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - class LeaderboardDataController extends DataController { public async function genGenerateData(): Awaitable { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + $leaderboard_data = (object) array(); // If refresing is disabled, exit @@ -17,9 +18,18 @@ class LeaderboardDataController extends DataController { exit(1); } - $leaders = await MultiTeam::genLeaderboard(); - $my_team = await MultiTeam::genTeam(SessionUtils::sessionTeam()); - $my_rank = await Team::genMyRank(SessionUtils::sessionTeam()); + list($leaders, list($my_team, $my_rank), $leaderboard_limit) = + await \HH\Asio\va( + MultiTeam::genLeaderboard(), + MultiTeam::genMyTeamRank(SessionUtils::sessionTeam()), + Configuration::gen('leaderboard_limit'), + ); + + $leaderboard_limit_value = intval($leaderboard_limit->getValue()); + if ($my_rank >= $leaderboard_limit_value) { + $my_rank = $leaderboard_limit_value."+"; + } + $my_team_data = (object) array( 'badge' => $my_team->getLogo(), 'points' => $my_team->getPoints(), @@ -51,4 +61,4 @@ class LeaderboardDataController extends DataController { } $leaderboardData = new LeaderboardDataController(); -\HH\Asio\join($leaderboardData->genGenerateData()); +$leaderboardData->sendData(); diff --git a/src/data/livesync.php b/src/data/livesync.php index 433fcef4..4b5983c1 100644 --- a/src/data/livesync.php +++ b/src/data/livesync.php @@ -8,13 +8,10 @@ class LiveSyncDataController extends DataController { $data = array(); await tr_start(); $input_auth_key = idx(Utils::getGET(), 'auth', ''); - $livesync_awaits = Map { - 'livesync_enabled' => Configuration::gen('livesync'), - 'livesync_auth_key' => Configuration::gen('livesync_auth_key'), - }; - $livesync_awaits_results = await \HH\Asio\m($livesync_awaits); - $livesync_enabled = $livesync_awaits_results['livesync_enabled']; - $livesync_auth_key = $livesync_awaits_results['livesync_auth_key']; + list($livesync_enabled, $livesync_auth_key) = await \HH\Asio\va( + Configuration::gen('livesync'), + Configuration::gen('livesync_auth_key'), + ); if ($livesync_enabled->getValue() === '1' && hash_equals( @@ -59,21 +56,49 @@ class LiveSyncDataController extends DataController { $team_livesync_exists = Map {}; $team_livesync_key = Map {}; foreach ($all_teams as $team) { + $team_livesync_types = Map {}; $team_id = $team->getId(); - $team_livesync_exists->add( - Pair {$team_id, Team::genLiveSyncExists($team_id, "fbctf")}, + + $team_livesync_types->add( + Pair {'fbctf', Team::genLiveSyncExists($team_id, 'fbctf')}, + ); + $team_livesync_types->add( + Pair { + 'facebook_oauth', + Team::genLiveSyncExists($team_id, 'facebook_oauth'), + }, + ); + $team_livesync_types->add( + Pair { + 'google_oauth', + Team::genLiveSyncExists($team_id, 'google_oauth'), + }, ); + + $team_livesync_exists->add(Pair {$team_id, $team_livesync_types}); } - $team_livesync_exists_results = await \HH\Asio\m($team_livesync_exists); - foreach ($team_livesync_exists_results as $team_id => $livesync_exists) { - if ($livesync_exists === true) { - $team_livesync_key->add( - Pair {$team_id, Team::genGetLiveSyncKey($team_id, "fbctf")}, - ); + + foreach ($team_livesync_exists as $team_id => $livesync_types) { + $team_livesync_keys = Map {}; + $team_livesync_exists_results = await \HH\Asio\m($livesync_types); // TODO: Combine Awaits + foreach ($team_livesync_exists_results as + $livesync_type => $livesync_exists) { + if (boolval($livesync_exists) === true) { + $team_livesync_keys->add( + Pair { + $livesync_type, + Team::genGetLiveSyncKey($team_id, $livesync_type), + }, + ); + } } + $team_livesync_keys->add( + Pair {'general', Team::genGetLiveSyncKey($team_id, 'general')}, + ); + $team_livesync_keys_results = await \HH\Asio\m($team_livesync_keys); // TODO: Combine Awaits + $team_livesync_key->add(Pair {$team_id, $team_livesync_keys_results}); } - $team_livesync_key_results = await \HH\Asio\m($team_livesync_key); - $teams_array = $team_livesync_key_results->toArray(); + $teams_array = $team_livesync_key->toArray(); $scores_array = array(); $scored_teams = array(); @@ -83,13 +108,21 @@ class LiveSyncDataController extends DataController { false) { continue; } - $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['timestamp'] = - $score->getTs(); - $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['capture'] = - true; - $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['hint'] = - false; - $scored_teams[$score->getLevelId()][] = $score->getTeamId(); + $team_livesync_array_scores = + $team_livesync_key->get($score->getTeamId()); + invariant( + $team_livesync_array_scores instanceof Map, + 'team_livesync_array_scores should of type Map and not null', + ); + foreach ($team_livesync_array_scores as + $livesync_type => $livesync_key) { + $scores_array[$score->getLevelId()][$livesync_key]['timestamp'] = + $score->getTs(); + $scores_array[$score->getLevelId()][$livesync_key]['capture'] = + true; + $scores_array[$score->getLevelId()][$livesync_key]['hint'] = false; + $scored_teams[$score->getLevelId()][] = $score->getTeamId(); + } } foreach ($all_hints as $hint) { if ($hint->getPenalty()) { @@ -97,17 +130,25 @@ class LiveSyncDataController extends DataController { false) { continue; } - $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['hint'] = - true; - if (in_array( - $hint->getTeamId(), - $scored_teams[$hint->getLevelId()], - ) === - false) { - $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['capture'] = - false; - $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['timestamp'] = - $hint->getTs(); + $team_livesync_array_hints = + $team_livesync_key->get($hint->getTeamId()); + invariant( + $team_livesync_array_hints instanceof Map, + 'team_livesync_array_hints should of type Map and not null', + ); + foreach ($team_livesync_array_hints as + $livesync_type => $livesync_key) { + $scores_array[$hint->getLevelId()][$livesync_key]['hint'] = true; + if (in_array( + $hint->getTeamId(), + $scored_teams[$hint->getLevelId()], + ) === + false) { + $scores_array[$hint->getLevelId()][$livesync_key]['capture'] = + false; + $scores_array[$hint->getLevelId()][$livesync_key]['timestamp'] = + $hint->getTs(); + } } } } diff --git a/src/data/map-data.php b/src/data/map-data.php index e781fda3..c3f8a9f2 100644 --- a/src/data/map-data.php +++ b/src/data/map-data.php @@ -2,41 +2,41 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - class MapDataController extends DataController { public async function genGenerateData(): Awaitable { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + $map_data = (object) array(); $my_team_id = SessionUtils::sessionTeam(); $my_name = SessionUtils::sessionTeamName(); - $all_levels = await Level::genAllLevels(); - $enabled_countries = await Country::genAllEnabledCountriesForMap(); - - $levels_map = Map {}; - foreach ($all_levels as $level) { - $levels_map[$level->getEntityId()] = $level; - } + list($all_levels, $enabled_countries) = await \HH\Asio\va( + Level::genAllLevelsCountryMap(), + Country::genAllEnabledCountriesForMap(), + ); foreach ($enabled_countries as $country) { - $country_level = $levels_map->get($country->getId()); + $country_level = $all_levels->get($country->getId()); $is_active_level = $country_level !== null && $country_level->getActive(); $active = ($country->getUsed() && $is_active_level) ? 'active' : ''; if ($country_level) { - $my_previous_score = await ScoreLog::genPreviousScore( - $country_level->getId(), - $my_team_id, - false, - ); - $other_previous_score = await ScoreLog::genPreviousScore( - $country_level->getId(), - $my_team_id, - true, - ); + list($my_previous_score, $other_previous_score) = await \HH\Asio\va( + ScoreLog::genAllPreviousScore( + $country_level->getId(), + $my_team_id, + false, + ), + ScoreLog::genPreviousScore( + $country_level->getId(), + $my_team_id, + true, + ), + ); // TODO: Combine Awaits // If my team has scored if ($my_previous_score) { @@ -46,7 +46,7 @@ class MapDataController extends DataController { } else if ($other_previous_score) { $captured_by = 'opponent'; $completed_by = - await MultiTeam::genCompletedLevel($country_level->getId()); + await MultiTeam::genCompletedLevel($country_level->getId()); // TODO: Combine Awaits $data_captured = ''; foreach ($completed_by as $c) { $data_captured .= ' '.$c->getName(); @@ -74,4 +74,4 @@ class MapDataController extends DataController { } $map = new MapDataController(); -\HH\Asio\join($map->genGenerateData()); +$map->sendData(); diff --git a/src/data/scores.php b/src/data/scores.php index f7d95a88..b29300e4 100644 --- a/src/data/scores.php +++ b/src/data/scores.php @@ -2,20 +2,21 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - class ScoresDataController extends DataController { public async function genGenerateData(): Awaitable { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + $data = array(); - $leaderboard = await MultiTeam::genLeaderboard(); + $leaderboard = await MultiTeam::genLeaderboard(false); foreach ($leaderboard as $team) { $values = array(); $i = 1; $progressive_scoreboard = - await Progressive::genProgressiveScoreboard($team->getName()); + await Progressive::genProgressiveScoreboard($team->getName()); // TODO: Combine Awaits foreach ($progressive_scoreboard as $progress) { $score = (object) array('time' => $i, 'score' => $progress->getPoints()); @@ -35,5 +36,6 @@ class ScoresDataController extends DataController { } } +/* HH_IGNORE_ERROR[1002] */ $scoresData = new ScoresDataController(); -\HH\Asio\join($scoresData->genGenerateData()); +$scoresData->sendData(); diff --git a/src/data/session.php b/src/data/session.php new file mode 100644 index 00000000..fc45d31d --- /dev/null +++ b/src/data/session.php @@ -0,0 +1,19 @@ + { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + + $data = array('true'); + $this->jsonSend($data); + } +} + +/* HH_IGNORE_ERROR[1002] */ +$sessionControler = new SessionController(); +$sessionControler->sendData(); diff --git a/src/data/stats.php b/src/data/stats.php new file mode 100644 index 00000000..12017229 --- /dev/null +++ b/src/data/stats.php @@ -0,0 +1,83 @@ + { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + SessionUtils::enforceAdmin(); + + $stats = array(); + + $awaitables = Map { + 'team_stats' => MultiTeam::genAllTeamsCache(), + 'session_stats' => Session::genAllSessions(), + 'level_stats' => Level::genAllLevels(), + 'active_level_stats' => Level::genAllActiveLevels(), + 'hint_stats' => HintLog::genAllHints(), + 'capture_stats' => ScoreLog::genAllScores(), + }; + $awaitables_results = await \HH\Asio\m($awaitables); + + $team_stats = $awaitables['team_stats']; + $session_stats = $awaitables['session_stats']; + $level_stats = $awaitables['level_stats']; + $active_level_stats = $awaitables['active_level_stats']; + $hint_stats = $awaitables['hint_stats']; + $capture_stats = $awaitables['capture_stats']; + + // Number of teams + $stats['teams'] = count($team_stats); + + // Number of active sessions + $stats['sessions'] = count($session_stats); + + // Number of levels + $stats['levels'] = count($level_stats); + + // Number of active levels + $stats['active_levels'] = count($active_level_stats); + + // Number of captures + $stats['hints'] = count($hint_stats); + + // Number of hints + $stats['captures'] = count($capture_stats); + + // AsyncMysqlConnectionPool Stats + $stats['database'] = Db::getDatabaseStats(); + + // Memcached Stats + $stats['memcached'] = Model::getMemcachedStats(); + + // System load average + $stats['load'] = sys_getloadavg(); + + // System CPU stats + $cpu_stats_1 = file('/proc/stat'); + sleep(1); + $cpu_stats_2 = file('/proc/stat'); + $cpu_info_1 = explode(" ", preg_replace("!cpu +!", "", $cpu_stats_1[0])); + $cpu_info_2 = explode(" ", preg_replace("!cpu +!", "", $cpu_stats_2[0])); + $cpu_diff = array(); + $cpu_diff['user'] = $cpu_info_2[0] - $cpu_info_1[0]; + $cpu_diff['nice'] = $cpu_info_2[1] - $cpu_info_1[1]; + $cpu_diff['sys'] = $cpu_info_2[2] - $cpu_info_1[2]; + $cpu_diff['idle'] = $cpu_info_2[3] - $cpu_info_1[3]; + $cpu_total = array_sum($cpu_diff); + $cpu_stats = array(); + foreach ($cpu_diff as $x => $y) + $cpu_stats[$x] = round($y / $cpu_total * 100, 1); + $stats['cpu'] = $cpu_stats; + + $this->jsonSend($stats); + } +} + +/* HH_IGNORE_ERROR[1002] */ +$statsController = new StatsController(); +$statsController->sendData(); diff --git a/src/data/teams.php b/src/data/teams.php index b4b176cd..a904295a 100644 --- a/src/data/teams.php +++ b/src/data/teams.php @@ -2,29 +2,45 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - class TeamDataController extends DataController { public async function genGenerateData(): Awaitable { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + $rank = 1; - $leaderboard = await MultiTeam::genLeaderboard(); + list($leaderboard, $gameboard, $leaderboard_limit) = await \HH\Asio\va( + MultiTeam::genLeaderboard(), + Configuration::gen('gameboard'), + Configuration::gen('leaderboard_limit'), + ); + $teams_data = (object) array(); // If refresing is disabled, exit - $gameboard = await Configuration::gen('gameboard'); if ($gameboard->getValue() === '0') { $this->jsonSend($teams_data); exit(1); } - foreach ($leaderboard as $team) { - $base = await MultiTeam::genPointsByType($team->getId(), 'base'); - $quiz = await MultiTeam::genPointsByType($team->getId(), 'quiz'); - $flag = await MultiTeam::genPointsByType($team->getId(), 'flag'); + $leaderboard_size = count($leaderboard); + $leaderboard_limit_value = intval($leaderboard_limit->getValue()); + if (($leaderboard_size <= $leaderboard_limit_value) || + ($leaderboard_limit_value === 0)) { + $leaderboard_count = $leaderboard_size; + } else { + $leaderboard_count = $leaderboard_limit_value; + } + for ($i = 0; $i < $leaderboard_count; $i++) { + $team = $leaderboard[$i]; + list($base, $quiz, $flag) = await \HH\Asio\va( + MultiTeam::genPointsByType($team->getId(), 'base'), + MultiTeam::genPointsByType($team->getId(), 'quiz'), + MultiTeam::genPointsByType($team->getId(), 'flag'), + ); - $logo_model = await $team->getLogoModel(); + $logo_model = await $team->getLogoModel(); // TODO: Combine Awaits $team_data = (object) array( 'logo' => array( @@ -54,4 +70,4 @@ class TeamDataController extends DataController { } $teamsData = new TeamDataController(); -\HH\Asio\join($teamsData->genGenerateData()); +$teamsData->sendData(); diff --git a/src/error.php b/src/error.php index 942892bd..eb264021 100644 --- a/src/error.php +++ b/src/error.php @@ -1,4 +1,42 @@ + + + An Error Occured + + + + + + +

An Error Occured!

+
+

Please check your request and try again.

+

If the problem persists, you should contact an admin.

+
+ +
+ + +END +; diff --git a/src/inc/gameboard/listview.php b/src/inc/gameboard/listview.php index b88d01e3..d2d769dc 100644 --- a/src/inc/gameboard/listview.php +++ b/src/inc/gameboard/listview.php @@ -2,25 +2,28 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - -class ListviewController { +class ListviewController extends ModuleController { public async function genRender(): Awaitable<:xhp> { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + await tr_start(); $listview_div =
; $listview_table =
; $active_levels = await Level::genAllActiveLevels(); foreach ($active_levels as $level) { - $country = await Country::gen(intval($level->getEntityId())); - $category = await Category::genSingleCategory($level->getCategoryId()); - $previous_score = await ScoreLog::genPreviousScore( - $level->getId(), - SessionUtils::sessionTeam(), - false, - ); + list($country, $category, $previous_score) = await \HH\Asio\va( + Country::gen(intval($level->getEntityId())), + Category::genSingleCategory($level->getCategoryId()), + ScoreLog::genAllPreviousScore( + $level->getId(), + SessionUtils::sessionTeam(), + false, + ), + ); // TODO: Combine Awaits if ($previous_score) { $span_status = {tr('Captured')}; @@ -45,5 +48,6 @@ class ListviewController { } } +/* HH_IGNORE_ERROR[1002] */ $listview_generated = new ListviewController(); -echo \HH\Asio\join($listview_generated->genRender()); +$listview_generated->sendRender(); diff --git a/src/inc/gameboard/modules/activity-viewmode.php b/src/inc/gameboard/modules/activity-viewmode.php index 9b7af0c3..6f414513 100644 --- a/src/inc/gameboard/modules/activity-viewmode.php +++ b/src/inc/gameboard/modules/activity-viewmode.php @@ -2,13 +2,15 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -class ActivityViewModeModuleController { +class ActivityViewModeModuleController extends ModuleController { public async function genRender(): Awaitable<:xhp> { await tr_start(); $activity_ul =
    ; - $all_activity = await Control::genAllActivity(); - $config = await Configuration::gen('language'); + list($all_activity, $config) = await \HH\Asio\va( + Control::genAllActivity(), + Configuration::gen('language'), + ); $language = $config->getValue(); foreach ($all_activity as $score) { $translated_country = @@ -40,4 +42,4 @@ class ActivityViewModeModuleController { /* HH_IGNORE_ERROR[1002] */ $activity_generated = new ActivityViewModeModuleController(); -echo \HH\Asio\join($activity_generated->genRender()); +$activity_generated->sendRender(); diff --git a/src/inc/gameboard/modules/activity.php b/src/inc/gameboard/modules/activity.php index c98bc5fd..1564cfa9 100644 --- a/src/inc/gameboard/modules/activity.php +++ b/src/inc/gameboard/modules/activity.php @@ -2,22 +2,32 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - -class ActivityModuleController { +class ActivityModuleController extends ModuleController { public async function genRender(): Awaitable<:xhp> { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + await tr_start(); $activity_ul =
      ; - $all_activity = await ActivityLog::genAllActivity(); - $config = await Configuration::gen('language'); + list($all_activity, $config) = await \HH\Asio\va( + ActivityLog::genAllActivity(), + Configuration::gen('language'), + ); $language = $config->getValue(); - foreach ($all_activity as $activity) { + $activity_count = count($all_activity); + $activity_limit = ($activity_count > 100) ? 100 : $activity_count; + for ($i = 0; $i < $activity_limit; $i++) { + $activity = $all_activity[$i]; $subject = $activity->getSubject(); $entity = $activity->getEntity(); $ts = $activity->getTs(); + $visible = $activity->getVisible(); + if ($visible === false) { + continue; + } if (($subject !== '') && ($entity !== '')) { $class_li = ''; $class_span = ''; @@ -78,5 +88,6 @@ class ActivityModuleController { } } +/* HH_IGNORE_ERROR[1002] */ $activity_generated = new ActivityModuleController(); -echo \HH\Asio\join($activity_generated->genRender()); +$activity_generated->sendRender(); diff --git a/src/inc/gameboard/modules/announcements.php b/src/inc/gameboard/modules/announcements.php index a7b3b2d4..673d8379 100644 --- a/src/inc/gameboard/modules/announcements.php +++ b/src/inc/gameboard/modules/announcements.php @@ -2,12 +2,13 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - -class AnnouncementsModuleController { +class AnnouncementsModuleController extends ModuleController { public async function genRender(): Awaitable<:xhp> { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + await tr_start(); $announcements = await Announcement::genAllAnnouncements(); $announcements_ul =
        ; @@ -41,5 +42,6 @@ class AnnouncementsModuleController { } } +/* HH_IGNORE_ERROR[1002] */ $announcements_generated = new AnnouncementsModuleController(); -echo \HH\Asio\join($announcements_generated->genRender()); +$announcements_generated->sendRender(); diff --git a/src/inc/gameboard/modules/controller.php b/src/inc/gameboard/modules/controller.php new file mode 100644 index 00000000..368f3f2f --- /dev/null +++ b/src/inc/gameboard/modules/controller.php @@ -0,0 +1,14 @@ +; + + public function sendRender(): void { + try { + echo \HH\Asio\join($this->genRender()); + } catch (RedirectException $e) { + echo ''; + } + } +} diff --git a/src/inc/gameboard/modules/filter.php b/src/inc/gameboard/modules/filter.php index 739381d6..95ff9c93 100644 --- a/src/inc/gameboard/modules/filter.php +++ b/src/inc/gameboard/modules/filter.php @@ -2,12 +2,13 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - -class FilterModuleController { +class FilterModuleController extends ModuleController { public async function genRender(): Awaitable<:xhp> { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + await tr_start(); $categories_ul =
          ; $categories_ul->appendChild( @@ -134,5 +135,6 @@ class="click-effect"> } } +/* HH_IGNORE_ERROR[1002] */ $filter_generated = new FilterModuleController(); -echo \HH\Asio\join($filter_generated->genRender()); +$filter_generated->sendRender(); diff --git a/src/inc/gameboard/modules/game-clock.php b/src/inc/gameboard/modules/game-clock.php index 48098d9c..69027b77 100644 --- a/src/inc/gameboard/modules/game-clock.php +++ b/src/inc/gameboard/modules/game-clock.php @@ -2,10 +2,7 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); - -class ClockModuleController { +class ClockModuleController extends ModuleController { private async function genGenerateIndicator( string $start_ts, string $end_ts, @@ -44,13 +41,20 @@ class ClockModuleController { } public async function genRender(): Awaitable<:xhp> { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + await tr_start(); - $timer = await Configuration::gen('timer'); - $start_ts = await Configuration::gen('start_ts'); - $end_ts = await Configuration::gen('end_ts'); - $timer = $timer->getValue(); - $start_ts = $start_ts->getValue(); - $end_ts = $end_ts->getValue(); + list($config_timer, $config_start_ts, $config_end_ts) = + await \HH\Asio\va( + Configuration::gen('timer'), + Configuration::gen('start_ts'), + Configuration::gen('end_ts'), + ); + $timer = $config_timer->getValue(); + $start_ts = $config_start_ts->getValue(); + $end_ts = $config_end_ts->getValue(); $now = time(); $init = intval($end_ts) - $now; @@ -142,5 +146,6 @@ class ClockModuleController { } } +/* HH_IGNORE_ERROR[1002] */ $clock_generated = new ClockModuleController(); -echo \HH\Asio\join($clock_generated->genRender()); +$clock_generated->sendRender(); diff --git a/src/inc/gameboard/modules/leaderboard-viewmode.php b/src/inc/gameboard/modules/leaderboard-viewmode.php index 3947114d..67658c8f 100644 --- a/src/inc/gameboard/modules/leaderboard-viewmode.php +++ b/src/inc/gameboard/modules/leaderboard-viewmode.php @@ -2,7 +2,7 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -class LeaderboardModuleViewController { +class LeaderboardModuleViewController extends ModuleController { public async function genRender(): Awaitable<:xhp> { await tr_start(); $leaderboard_ul =
            ; @@ -46,4 +46,4 @@ class LeaderboardModuleViewController { /* HH_IGNORE_ERROR[1002] */ $leaderboard_generated = new LeaderboardModuleViewController(); -echo \HH\Asio\join($leaderboard_generated->genRender()); +$leaderboard_generated->sendRender(); diff --git a/src/inc/gameboard/modules/leaderboard.php b/src/inc/gameboard/modules/leaderboard.php index ee67b599..bc634f0a 100644 --- a/src/inc/gameboard/modules/leaderboard.php +++ b/src/inc/gameboard/modules/leaderboard.php @@ -2,20 +2,23 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - -class LeaderboardModuleController { +class LeaderboardModuleController extends ModuleController { public async function genRender(): Awaitable<:xhp> { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + await tr_start(); $leaderboard_ul =
              ; - $my_team = await MultiTeam::genTeam(SessionUtils::sessionTeam()); - $my_rank = await Team::genMyRank(SessionUtils::sessionTeam()); + list($my_team, $my_rank, $gameboard) = await \HH\Asio\va( + MultiTeam::genTeam(SessionUtils::sessionTeam()), + Team::genMyRank(SessionUtils::sessionTeam()), + Configuration::gen('gameboard'), + ); // If refresing is enabled, do the needful - $gameboard = await Configuration::gen('gameboard'); if ($gameboard->getValue() === '1') { $leaders = await MultiTeam::genLeaderboard(); $rank = 1; @@ -24,7 +27,7 @@ class LeaderboardModuleController { $team = $leaders[$i]; // TODO also duplicated in modules/teams.php. Needs to be un-duplicated. - $logo_model = await $team->getLogoModel(); + $logo_model = await $team->getLogoModel(); // TODO: Combine Awaits if ($logo_model->getCustom()) { $image = getLogo()}>; @@ -56,6 +59,15 @@ class LeaderboardModuleController { } } + if ($my_team->getVisible() === true) { + $leaderboard_limit = await Configuration::gen('leaderboard_limit'); + if ($my_rank >= intval($leaderboard_limit->getValue())) { + $my_rank = intval($leaderboard_limit->getValue()) + 1 ."+"; + } + } else { + $my_rank = "N/A"; + } + return
              @@ -81,5 +93,6 @@ class LeaderboardModuleController { } } +/* HH_IGNORE_ERROR[1002] */ $leaderboard_generated = new LeaderboardModuleController(); -echo \HH\Asio\join($leaderboard_generated->genRender()); +$leaderboard_generated->sendRender(); diff --git a/src/inc/gameboard/modules/teams.php b/src/inc/gameboard/modules/teams.php index f8328308..88e32a2e 100644 --- a/src/inc/gameboard/modules/teams.php +++ b/src/inc/gameboard/modules/teams.php @@ -2,12 +2,13 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - -class TeamModuleController { +class TeamModuleController extends ModuleController { public async function genRender(): Awaitable<:xhp> { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + await tr_start(); $leaderboard = await MultiTeam::genLeaderboard(); $rank = 1; @@ -16,8 +17,19 @@ class TeamModuleController { $gameboard = await Configuration::gen('gameboard'); if ($gameboard->getValue() === '1') { - foreach ($leaderboard as $leader) { - $logo_model = await $leader->getLogoModel(); + $leaderboard_size = count($leaderboard); + $leaderboard_limit = await Configuration::gen('leaderboard_limit'); + $leaderboard_limit_value = intval($leaderboard_limit->getValue()); + + if (($leaderboard_size <= $leaderboard_limit_value) || + ($leaderboard_limit_value === 0)) { + $leaderboard_count = $leaderboard_size; + } else { + $leaderboard_count = $leaderboard_limit_value; + } + for ($i = 0; $i < $leaderboard_count; $i++) { + $leader = $leaderboard[$i]; + $logo_model = await $leader->getLogoModel(); // TODO: Combine Awaits if ($logo_model->getCustom()) { $image = getLogo()}>; @@ -66,5 +78,6 @@ class="click-effect">{tr('Everyone')} < } } +/* HH_IGNORE_ERROR[1002] */ $teams_generated = new TeamModuleController(); -echo \HH\Asio\join($teams_generated->genRender()); +$teams_generated->sendRender(); diff --git a/src/index.php b/src/index.php index c34a3428..e7be2691 100644 --- a/src/index.php +++ b/src/index.php @@ -7,7 +7,9 @@ $response = await Router::genRoute(); echo $response; } catch (RedirectException $e) { - error_log($e->getTraceAsString()); + error_log( + 'RedirectException: ('.get_class($e).') '.$e->getTraceAsString(), + ); http_response_code($e->getStatusCode()); Utils::redirect($e->getPath()); } diff --git a/src/models/ActivityLog.php b/src/models/ActivityLog.php index 9f860cb2..2cbcc7a3 100644 --- a/src/models/ActivityLog.php +++ b/src/models/ActivityLog.php @@ -18,6 +18,7 @@ private function __construct( private string $formatted_subject = '', private string $formatted_entity = '', private string $formatted_message = '', + private bool $visible = true, ) { $formatted_subject = \HH\Asio\join(self::genFormatString("%s", $this->subject)); @@ -28,6 +29,9 @@ private function __construct( $formatted_message = \HH\Asio\join(self::genFormatString($this->message, $this->arguments)); $this->formatted_message = $formatted_message; + $visible = + \HH\Asio\join(self::genLogEntryVisible($this->subject, $this->action)); + $this->visible = $visible; } public function getId(): int { @@ -70,6 +74,10 @@ public function getTs(): string { return $this->ts; } + public function getVisible(): bool { + return $this->visible; + } + private static function activitylogFromRow( Map $row, ): ActivityLog { @@ -97,7 +105,7 @@ private static function activitylogFromRow( case "Team": $team_exists = await Team::genTeamExistById(intval($id)); if ($team_exists === true) { - $team = await Team::genTeam(intval($id)); + $team = await MultiTeam::genTeam(intval($id)); $variables[] = $team->getName(); } else { return ''; @@ -133,6 +141,25 @@ private static function activitylogFromRow( return $string; } + public static async function genLogEntryVisible( + string $subject, + string $action, + ): Awaitable { + if ($subject === '') { + return true; + } + list($class, $id) = explode(':', $subject); + if ($class === 'Team' && $action === 'captured') { + $team_exists = await Team::genTeamExistById(intval($id)); + if ($team_exists === true) { + $team = await MultiTeam::genTeam(intval($id)); + return $team->getVisible(); + } else + return false; + } + return true; + } + public static async function genCreate( string $subject, string $action, @@ -191,8 +218,10 @@ private static function activitylogFromRow( string $entity_class, int $entity_id, ): Awaitable { - $config_game = await Configuration::gen('game'); - $config_pause = await Configuration::gen('game_paused'); + list($config_game, $config_pause) = await \HH\Asio\va( + Configuration::gen('game'), + Configuration::gen('game_paused'), + ); if ((intval($config_game->getValue()) === 1) && (intval($config_pause->getValue()) === 0)) { await self::genCreate( @@ -255,8 +284,9 @@ private static function activitylogFromRow( if (!$mc_result || count($mc_result) === 0 || $refresh) { $db = await self::genDb(); $activity_log_lines = array(); - $result = - await $db->queryf('SELECT * FROM activity_log ORDER BY ts DESC'); + $result = await $db->query( + 'SELECT * FROM activity_log ORDER BY ts DESC LIMIT 100', + ); foreach ($result->mapRows() as $row) { $activity_log = self::activitylogFromRow($row); if (($activity_log->getFormattedMessage() !== '') || diff --git a/src/models/Announcement.php b/src/models/Announcement.php index cd2b87ca..e7e3014d 100644 --- a/src/models/Announcement.php +++ b/src/models/Announcement.php @@ -18,7 +18,7 @@ public function getId(): int { } public function getAnnouncement(): string { - return $this->announcement; + return mb_convert_encoding($this->announcement, 'UTF-8'); } public function getTs(): string { @@ -50,8 +50,10 @@ private static function announcementFromRow( public static async function genCreateAuto( string $announcement, ): Awaitable { - $config_game = await Configuration::gen('game'); - $config_pause = await Configuration::gen('game_paused'); + list($config_game, $config_pause) = await \HH\Asio\va( + Configuration::gen('game'), + Configuration::gen('game_paused'), + ); if ((intval($config_game->getValue()) === 1) && (intval($config_pause->getValue()) === 0)) { $auto_announce = await Configuration::gen('auto_announce'); diff --git a/src/models/Attachment.php b/src/models/Attachment.php index fbb151f9..585ed233 100644 --- a/src/models/Attachment.php +++ b/src/models/Attachment.php @@ -11,6 +11,8 @@ class Attachment extends Model { 'LEVELS_COUNT' => 'attachment_levels_count', 'LEVEL_ATTACHMENTS' => 'attachment_levels', 'ATTACHMENTS' => 'attachments_by_id', + 'LEVEL_ATTACHMENTS_NAMES' => 'attachment_file_names', + 'LEVEL_ATTACHMENTS_LINKS' => 'attachment_file_links', }; private function __construct( @@ -62,14 +64,15 @@ public function getLevelId(): int { // Extract extension and name $parts = explode('.', $filename, 2); - $local_filename .= firstx($parts).'_'.$md5_str; + $local_filename .= + mb_convert_encoding(firstx($parts), 'UTF-8').'_'.$md5_str; $extension = idx($parts, 1); if ($extension !== null) { - $local_filename .= '.'.$extension; + $local_filename .= '.'.mb_convert_encoding($extension, 'UTF-8'); } - // Remove all non alpahnum characters from filename - allow international chars, dash, underscore, and period + // Remove all non alphanum characters from filename - allow international chars, dash, underscore, and period $local_filename = preg_replace('/[^\p{L}\p{N}_\-.]+/u', '_', $local_filename); @@ -197,6 +200,168 @@ public function getLevelId(): int { } } + public static async function genAllAttachmentsForGame( + bool $refresh = false, + ): Awaitable> { + $mc_result = self::getMCRecords('LEVEL_ATTACHMENTS'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $db = await self::genDb(); + $attachments = array(); + $result = await $db->queryf('SELECT * FROM attachments'); + foreach ($result->mapRows() as $row) { + $attachments[intval($row->get('level_id'))][] = + self::attachmentFromRow($row); + } + self::setMCRecords('LEVEL_ATTACHMENTS', new Map($attachments)); + $attachments = new Map($attachments); + invariant( + $attachments instanceof Map, + 'attachments should be a Map of Attachment', + ); + return $attachments; + } else { + invariant( + $mc_result instanceof Map, + 'cache return should be of type Map', + ); + return $mc_result; + } + } + + public static async function genAllAttachmentsFileNames( + int $level_id, + bool $refresh = false, + ): Awaitable> { + $mc_result = self::getMCRecords('LEVEL_ATTACHMENTS_NAMES'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $db = await self::genDb(); + $filenames = array(); + $attachments = await self::genAllAttachmentsForGame(); + invariant( + $attachments instanceof Map, + 'attachments should be a Map of Attachment', + ); + foreach ($attachments as $level => $attachment_arr) { + invariant( + is_array($attachment_arr), + 'attachment_arr should be an array of Attachment', + ); + foreach ($attachment_arr as $attach_obj) { + invariant( + $attach_obj instanceof Attachment, + 'link_obj should be of type Attachment', + ); + $filenames[$level][] = $attach_obj->getFilename(); + } + } + self::setMCRecords('LEVEL_ATTACHMENTS_NAMES', new Map($filenames)); + $filenames = new Map($filenames); + if ($filenames->contains($level_id)) { + $filename = $filenames->get($level_id); + invariant( + is_array($filename), + 'filename should be an array of string', + ); + return $filename; + } else { + return array(); + } + } else { + invariant( + $mc_result instanceof Map, + 'cache return should be of type Map', + ); + if ($mc_result->contains($level_id)) { + $filename = $mc_result->get($level_id); + invariant( + is_array($filename), + 'filename should be an array of string', + ); + return $filename; + } else { + return array(); + } + } + } + + public static async function genAllAttachmentsFileLinks( + int $level_id, + bool $refresh = false, + ): Awaitable> { + $mc_result = self::getMCRecords('LEVEL_ATTACHMENTS_LINKS'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $db = await self::genDb(); + $attachment_links = array(); + $attachments = await self::genAllAttachmentsForGame(); + invariant( + $attachments instanceof Map, + 'attachments should be a Map of Attachment', + ); + foreach ($attachments as $level => $attachment_arr) { + invariant( + is_array($attachment_arr), + 'attachment_arr should be an array of Attachment', + ); + foreach ($attachment_arr as $attach_obj) { + invariant( + $attach_obj instanceof Attachment, + 'link_obj should be of type Attachment', + ); + $attachment_links[$level][] = $attach_obj->getFileLink(); + } + } + self::setMCRecords( + 'LEVEL_ATTACHMENTS_LINKS', + new Map($attachment_links), + ); + $attachment_links = new Map($attachment_links); + if ($attachment_links->contains($level_id)) { + $attachment_link = $attachment_links->get($level_id); + invariant( + is_array($attachment_link), + 'attachment link should be an array of string', + ); + return $attachment_link; + } else { + return array(); + } + } else { + invariant( + $mc_result instanceof Map, + 'cache return should be of type Map', + ); + if ($mc_result->contains($level_id)) { + $attachment_link = $mc_result->get($level_id); + invariant( + is_array($attachment_link), + 'attachment_link should be an array of string', + ); + return $attachment_link; + } else { + return array(); + } + } + } + + public static async function genAllAttachmentsFileNamesLinks( + int $level_id, + bool $refresh = false, + ): Awaitable>> { + $filenames_links = array(); + list($file_names, $file_links) = await \HH\Asio\va( + self::genAllAttachmentsFileNames($level_id), + self::genAllAttachmentsFileLinks($level_id), + ); + + foreach ($file_names as $idx => $file_name) { + if (idx($file_links, $idx) !== null) { + $filenames_links[$idx]['filename'] = $file_name; + $filenames_links[$idx]['file_link'] = $file_links[$idx]; + } + } + return $filenames_links; + } + // Get a single attachment. /* HH_IGNORE_ERROR[4110]: Claims - It is incompatible with void because this async function implicitly returns Awaitable, yet this returns Awaitable and the type is checked on line 185 */ public static async function gen( diff --git a/src/models/Cache.php b/src/models/Cache.php new file mode 100644 index 00000000..08aa0d63 --- /dev/null +++ b/src/models/Cache.php @@ -0,0 +1,31 @@ + $CACHE = Map {}; + + public function __construct() {} + + public function setCache(string $key, mixed $value): void { + $this->CACHE->add(Pair {strval($key), $value}); + } + + public function getCache(string $key): mixed { + if ($this->CACHE->contains($key)) { + return $this->CACHE->get($key); + } else { + return false; + } + } + + public function deleteCache(string $key): void { + if ($this->CACHE->contains($key)) { + $this->CACHE->remove($key); + } + } + + public function flushCache(): void { + $this->CACHE = Map {}; + } + +} diff --git a/src/models/Configuration.php b/src/models/Configuration.php index dc344e40..25f1623f 100644 --- a/src/models/Configuration.php +++ b/src/models/Configuration.php @@ -2,7 +2,16 @@ class Configuration extends Model { - const string MC_KEY = 'configuration:'; + protected static string $MC_KEY = 'configuration:'; + + protected static Map + $MC_KEYS = Map { + 'CONFIGURATION' => 'config_field', + 'FACEBOOK_INTEGRATION_APP_ID' => 'integration_facebook_app_id', + 'FACEBOOK_INTEGRATION_APP_SECRET' => + 'integration_facebook_app_secret', + 'GOOGLE_INTEGRATION_FILE' => 'integration_google_file', + }; private function __construct( private int $id, @@ -28,23 +37,52 @@ public function getDescription(): string { } // Get configuration entry. - public static async function gen(string $field): Awaitable { - $mc = self::getMc(); - $key = self::MC_KEY.$field; - $mc_result = $mc->get($key); - if ($mc_result) { - $result = $mc_result; - } else { + public static async function gen( + string $field, + bool $refresh = false, + ): Awaitable { + $mc_result = self::getMCRecords('CONFIGURATION'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { $db = await self::genDb(); - $db_result = await $db->queryf( - 'SELECT * FROM configuration WHERE field = %s LIMIT 1', + $config_values = Map {}; + $result = await $db->queryf('SELECT * FROM configuration'); + foreach ($result->mapRows() as $row) { + $config_values->add( + Pair { + strval($row->get('field')), + self::configurationFromRow($row->toArray()), + }, + ); + } + self::setMCRecords('CONFIGURATION', $config_values); + invariant( + $config_values->contains($field) !== false, + 'config value not found (db): %s', $field, ); - invariant($db_result->numRows() === 1, 'Expected exactly one result'); - $result = firstx($db_result->mapRows())->toArray(); - $mc->set($key, $result); + $config = $config_values->get($field); + invariant( + $config instanceof Configuration, + 'config cache value should of type Configuration and not null', + ); + return $config; + } else { + invariant( + $mc_result instanceof Map, + 'config cache return should be of type Map and not null', + ); + invariant( + $mc_result->contains($field) !== false, + 'config value not found (cache): %s', + $field, + ); + $config = $mc_result->get($field); + invariant( + $config instanceof Configuration, + 'config cache value should of type Configuration and not null', + ); + return $config; } - return self::configurationFromRow($result); } // Change configuration field. @@ -62,7 +100,7 @@ public function getDescription(): string { await Session::genDeleteAllUnprotected(); } - self::getMc()->delete(self::MC_KEY.$field); + self::invalidateMCRecords(); // Invalidate Configuration data. } // Check if field is valid. @@ -96,10 +134,11 @@ public function getDescription(): string { public static async function genCurrentPasswordType( ): Awaitable { $db = await self::genDb(); - $db_result = await $db->queryf( - 'SELECT * FROM password_types WHERE field = (SELECT value FROM configuration WHERE field = %s) LIMIT 1', - 'password_type' - ); + $db_result = + await $db->queryf( + 'SELECT * FROM password_types WHERE field = (SELECT value FROM configuration WHERE field = %s) LIMIT 1', + 'password_type', + ); invariant($db_result->numRows() === 1, 'Expected exactly one result'); $result = firstx($db_result->mapRows())->toArray(); @@ -132,26 +171,69 @@ private static function configurationFromRow( ); } - public static function genGoogleOAuthFileExists(): bool { - $settings_file = '../../settings.ini'; - $config = parse_ini_file($settings_file); + public static function getFacebookOAuthSettingsExists( + bool $refresh = false, + ): bool { + return (self::getFacebookOAuthSettingsAppId($refresh) !== '' && + self::getFacebookOAuthSettingsAppSecret($refresh) !== ''); + } - if ((array_key_exists('GOOGLE_OAUTH_FILE', $config) === true) && - (file_exists($config['GOOGLE_OAUTH_FILE']) === true)) { - return true; + public static function getFacebookOAuthSettingsAppId( + bool $refresh = false, + ): string { + $mc_result = self::getMCRecords('FACEBOOK_INTEGRATION_APP_ID'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $settings_file = '../../settings.ini'; + $config = parse_ini_file($settings_file); + $app_id = ''; + if (array_key_exists('FACEBOOK_OAUTH_APP_ID', $config) === true) { + $app_id = strval($config['FACEBOOK_OAUTH_APP_ID']); + } + self::setMCRecords('FACEBOOK_INTEGRATION_APP_ID', $app_id); + return $app_id; + } else { + return strval($mc_result); } - return false; } - public static function genGoogleOAuthFile(): string { - $settings_file = '../../settings.ini'; - $config = parse_ini_file($settings_file); - - if ((array_key_exists('GOOGLE_OAUTH_FILE', $config) === true) && - (file_exists($config['GOOGLE_OAUTH_FILE']) === true)) { - return strval($config['GOOGLE_OAUTH_FILE']); + public static function getFacebookOAuthSettingsAppSecret( + bool $refresh = false, + ): string { + $mc_result = self::getMCRecords('FACEBOOK_INTEGRATION_APP_SECRET'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $settings_file = '../../settings.ini'; + $config = parse_ini_file($settings_file); + $app_secret = ''; + if (array_key_exists('FACEBOOK_OAUTH_APP_SECRET', $config) === true) { + $app_secret = strval($config['FACEBOOK_OAUTH_APP_SECRET']); + } + self::setMCRecords('FACEBOOK_INTEGRATION_APP_SECRET', $app_secret); + return $app_secret; + } else { + return strval($mc_result); } + } + + public static function getGoogleOAuthFileExists( + bool $refresh = false, + ): bool { + return (self::getGoogleOAuthFile($refresh) !== ''); + } - return ''; + public static function getGoogleOAuthFile(bool $refresh = false): string { + $mc_result = self::getMCRecords('GOOGLE_INTEGRATION_FILE'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $settings_file = '../../settings.ini'; + $config = parse_ini_file($settings_file); + $oauth_file = ''; + if ((array_key_exists('GOOGLE_OAUTH_FILE', $config) === true) && + (file_exists($config['GOOGLE_OAUTH_FILE']) === true)) { + $oauth_file = strval($config['GOOGLE_OAUTH_FILE']); + } + self::setMCRecords('GOOGLE_INTEGRATION_FILE', $oauth_file); + return $oauth_file; + } else { + return strval($mc_result); + } } } diff --git a/src/models/Control.php b/src/models/Control.php index 57d0aa32..93c6b7a1 100644 --- a/src/models/Control.php +++ b/src/models/Control.php @@ -7,93 +7,97 @@ class Control extends Model { protected static Map $MC_KEYS = Map {'ALL_ACTIVITY' => 'activity'}; + public static async function genServerAddr(): Awaitable { + $host = gethostname(); + $ip = gethostbyname($host); + return strval($ip); + } + public static async function genStartScriptLog( int $pid, string $name, string $cmd, ): Awaitable { $db = await self::genDb(); + $host = await Control::genServerAddr(); await $db->queryf( - 'INSERT INTO scripts (ts, pid, name, cmd, status) VALUES (NOW(), %d, %s, %s, 1)', + 'INSERT INTO scripts (ts, pid, name, host, cmd, status) VALUES (NOW(), %d, %s, %s, %s, 1)', $pid, $name, + $host, $cmd, ); } public static async function genStopScriptLog(int $pid): Awaitable { $db = await self::genDb(); + $host = await Control::genServerAddr(); await $db->queryf( - 'UPDATE scripts SET status = 0 WHERE pid = %d LIMIT 1', + 'UPDATE scripts SET status = 0, host = %s WHERE pid = %d LIMIT 1', + $host, $pid, ); } public static async function genScriptPid(string $name): Awaitable { $db = await self::genDb(); - $result = await $db->queryf( - 'SELECT pid FROM scripts WHERE name = %s AND status = 1 LIMIT 1', - $name, - ); - return intval(must_have_idx($result->mapRows()[0], 'pid')); + $host = await Control::genServerAddr(); + $result = + await $db->queryf( + 'SELECT pid FROM scripts WHERE name = %s AND host = %s AND status = 1 LIMIT 1', + $name, + $host, + ); + $pid = 0; + if ($result->numRows() > 0) { + $pid = intval(must_have_idx($result->mapRows()[0], 'pid')); + } + return $pid; } public static async function genClearScriptLog(): Awaitable { $db = await self::genDb(); - await $db->queryf('DELETE FROM scripts WHERE id > 0 AND status = 0'); + $host = await Control::genServerAddr(); + await $db->queryf( + 'DELETE FROM scripts WHERE id > 0 AND status = 0 AND host = %s', + $host, + ); } public static async function genBegin(): Awaitable { - // Disable registration - await Configuration::genUpdate('registration', '0'); - - // Clear announcements log - await Announcement::genDeleteAll(); - - // Clear activity log - await ActivityLog::genDeleteAll(); - - // Announce game starting - await Announcement::genCreateAuto('Game has started!'); - - // Log game starting - await ActivityLog::genCreateGenericLog('Game has started!'); - - // Reset all points - await Team::genResetAllPoints(); - - // Clear scores log - await ScoreLog::genResetScores(); - - // Clear hints log - await HintLog::genResetHints(); - - // Clear failures log - await FailureLog::genResetFailures(); - - // Clear bases log - await self::genResetBases(); - await self::genClearScriptLog(); - - // Mark game as started - await Configuration::genUpdate('game', '1'); + await \HH\Asio\va( + Announcement::genDeleteAll(), // Clear announcements log + ActivityLog::genDeleteAll(), // Clear activity log + Team::genResetAllPoints(), // Reset all points + ScoreLog::genResetScores(), // Clear scores log + HintLog::genResetHints(), // Clear hints log + FailureLog::genResetFailures(), // Clear failures log + self::genResetBases(), // Clear bases log + self::genClearScriptLog(), + Configuration::genUpdate('registration', '0'), // Disable registration + ); - // Enable scoring - await Configuration::genUpdate('scoring', '1'); + await \HH\Asio\va( + Announcement::genCreateAuto('Game has started!'), // Announce game starting + ActivityLog::genCreateGenericLog('Game has started!'), // Log game starting + Configuration::genUpdate('game', '1'), // Mark game as started + Configuration::genUpdate('scoring', '1'), // Enable scoring + ); // Take timestamp of start $start_ts = time(); await Configuration::genUpdate('start_ts', strval($start_ts)); // Calculate timestamp of the end or game duration - $config = await Configuration::gen('end_ts'); - $end_ts = intval($config->getValue()); - + $config_end_ts = await Configuration::gen('end_ts'); + $end_ts = intval($config_end_ts->getValue()); if ($end_ts === 0) { - $config = await Configuration::gen('game_duration_value'); - $duration_value = intval($config->getValue()); - $config = await Configuration::gen('game_duration_unit'); - $duration_unit = $config->getValue(); + list($config_value, $config_unit) = await \HH\Asio\va( + Configuration::gen('game_duration_value'), + Configuration::gen('game_duration_unit'), + ); + $duration_value = intval($config_value->getValue()); + $duration_unit = $config_unit->getValue(); switch ($duration_unit) { case 'd': $duration = $duration_value * 60 * 60 * 24; @@ -109,63 +113,48 @@ class Control extends Model { await Configuration::genUpdate('end_ts', strval($end_ts)); } else { $duration_length = ($end_ts - $start_ts) / 60; - await Configuration::genUpdate( - 'game_duration_value', - strval($duration_length), + await \HH\Asio\va( + Configuration::genUpdate( + 'game_duration_value', + strval($duration_length), + ), + Configuration::genUpdate('game_duration_unit', 'm'), ); - await Configuration::genUpdate('game_duration_unit', 'm'); } - // Set pause to zero - await Configuration::genUpdate('pause_ts', '0'); - - // Set game to not paused - await Configuration::genUpdate('game_paused', '0'); - - // Kick off timer - await Configuration::genUpdate('timer', '1'); - - // Reset and kick off progressive scoreboard - await Progressive::genReset(); - await Progressive::genRun(); + await \HH\Asio\va( + Configuration::genUpdate('pause_ts', '0'), // Set pause to zero + Configuration::genUpdate('game_paused', '0'), // Set game to not paused + Configuration::genUpdate('timer', '1'), // Kick off timer + Progressive::genReset(), // Reset and kick off progressive scoreboard + ); - // Kick off scoring for bases - await Level::genBaseScoring(); + await \HH\Asio\va( + Progressive::genRun(), + Level::genBaseScoring(), // Kick off scoring for bases + ); } public static async function genEnd(): Awaitable { - // Announce game ending - await Announcement::genCreateAuto('Game has ended!'); - - // Log game ending - await ActivityLog::genCreateGenericLog('Game has ended!'); - - // Mark game as finished and it stops progressive scoreboard - await Configuration::genUpdate('game', '0'); - - // Disable scoring - await Configuration::genUpdate('scoring', '0'); - - // Put timestampts to zero - await Configuration::genUpdate('start_ts', '0'); - await Configuration::genUpdate('end_ts', '0'); - await Configuration::genUpdate('next_game', '0'); - - // Set pause to zero - await Configuration::genUpdate('pause_ts', '0'); - - // Stop timer - await Configuration::genUpdate('timer', '0'); + await \HH\Asio\va( + Announcement::genCreateAuto('Game has ended!'), // Announce game ending + ActivityLog::genCreateGenericLog('Game has ended!'), // Log game ending + Configuration::genUpdate('game', '0'), // Mark game as finished and stop progressive scoreboard + Configuration::genUpdate('scoring', '0'), // Disable scoring + Configuration::genUpdate('start_ts', '0'), // Put timestamps to zero + Configuration::genUpdate('end_ts', '0'), + Configuration::genUpdate('next_game', '0'), + Configuration::genUpdate('pause_ts', '0'), // Set pause to zero + Configuration::genUpdate('timer', '0'), // Stop timer + ); $pause = await Configuration::gen('game_paused'); $game_paused = $pause->getValue() === '1'; if (!$game_paused) { // Stop bases scoring process - await Level::genStopBaseScoring(); - // Stop progressive scoreboard process - await Progressive::genStop(); + await \HH\Asio\va(Level::genStopBaseScoring(), Progressive::genStop()); } else { // Set game to not paused await Configuration::genUpdate('game_paused', '0'); @@ -173,46 +162,32 @@ class Control extends Model { } public static async function genPause(): Awaitable { - // Announce game starting - await Announcement::genCreateAuto('Game has been paused!'); - - // Log game paused - await ActivityLog::genCreateGenericLog('Game has been paused!'); - - // Disable scoring - await Configuration::genUpdate('scoring', '0'); + await \HH\Asio\va( + Announcement::genCreateAuto('Game has been paused!'), // Announce game paused + ActivityLog::genCreateGenericLog('Game has been paused!'), // Log game paused + Configuration::genUpdate('scoring', '0'), // Disable scoring + ); - // Set pause timestamp $pause_ts = time(); - await Configuration::genUpdate('pause_ts', strval($pause_ts)); - - // Set gane to paused - await Configuration::genUpdate('game_paused', '1'); - - // Stop timer - await Configuration::genUpdate('timer', '0'); - - // Stop bases scoring process - await Level::genStopBaseScoring(); - - // Stop progressive scoreboard process - await Progressive::genStop(); + await \HH\Asio\va( + Configuration::genUpdate('pause_ts', strval($pause_ts)), // Set pause timestamp + Configuration::genUpdate('game_paused', '1'), // Set gane to paused + Configuration::genUpdate('timer', '0'), // Stop timer + Level::genStopBaseScoring(), // Stop bases scoring process + Progressive::genStop(), // Stop progressive scoreboard process + ); } public static async function genUnpause(): Awaitable { - // Enable scoring - await Configuration::genUpdate('scoring', '1'); - - // Get pause time - $config_pause_ts = await Configuration::gen('pause_ts'); + await Configuration::genUpdate('scoring', '1'); // Enable scoring + list($config_pause_ts, $config_start_ts, $config_end_ts) = + await \HH\Asio\va( + Configuration::gen('pause_ts'), // Get pause time + Configuration::gen('start_ts'), // Get start time + Configuration::gen('end_ts'), // Get end time + ); $pause_ts = intval($config_pause_ts->getValue()); - - // Get start time - $config_start_ts = await Configuration::gen('start_ts'); $start_ts = intval($config_start_ts->getValue()); - - // Get end time - $config_end_ts = await Configuration::gen('end_ts'); $end_ts = intval($config_end_ts->getValue()); // Calulcate game remaining @@ -221,80 +196,64 @@ class Control extends Model { $remaining_duration = $game_duration - $game_played_duration; $end_ts = time() + $remaining_duration; - // Set new endtime - await Configuration::genUpdate('end_ts', strval($end_ts)); - - // Set pause to zero - await Configuration::genUpdate('pause_ts', '0'); - - // Set gane to not paused - await Configuration::genUpdate('game_paused', '0'); - - // Start timer - await Configuration::genUpdate('timer', '1'); - - // Kick off progressive scoreboard - await Progressive::genRun(); - - // Kick off scoring for bases - await Level::genBaseScoring(); - - // Announce game resumed - await Announcement::genCreateAuto('Game has resumed!'); - - // Log game paused - await ActivityLog::genCreateGenericLog('Game has resumed!'); + await \HH\Asio\va( + Configuration::genUpdate('end_ts', strval($end_ts)), // Set new endtime + Configuration::genUpdate('pause_ts', '0'), // Set pause to zero + Configuration::genUpdate('game_paused', '0'), // Set gane to not paused + Configuration::genUpdate('timer', '1'), // Start timer + Progressive::genRun(), // Kick off progressive scoreboard + Level::genBaseScoring(), // Kick off scoring for bases + Announcement::genCreateAuto('Game has resumed!'), // Announce game resumed + ActivityLog::genCreateGenericLog('Game has resumed!'), // Log game resumed + ); } public static async function genAutoBegin(): Awaitable { - // Get start time - $config_start_ts = await Configuration::gen('start_ts'); + // Prevent autorun.php from storing timestamps in local cache, forever (the script runs continuously). + Configuration::deleteLocalCache('CONFIGURATION'); + list($config_start_ts, $config_end_ts, $config_game_paused) = + await \HH\Asio\va( + Configuration::gen('start_ts'), // Get start time + Configuration::gen('end_ts'), // Get end time + Configuration::gen('game_paused'), // Get paused status + ); $start_ts = intval($config_start_ts->getValue()); - - // Get end time - $config_end_ts = await Configuration::gen('end_ts'); $end_ts = intval($config_end_ts->getValue()); - - // Get paused status - $config_game_paused = await Configuration::gen('game_paused'); $game_paused = intval($config_game_paused->getValue()); if (($game_paused === 0) && ($start_ts <= time()) && ($end_ts > time())) { - // Start the game - await Control::genBegin(); + await Control::genBegin(); // Start the game } } public static async function genAutoEnd(): Awaitable { - // Get start time - $config_start_ts = await Configuration::gen('start_ts'); + // Prevent autorun.php from storing timestamps in local cache, forever (the script runs continuously). + Configuration::deleteLocalCache('CONFIGURATION'); + list($config_start_ts, $config_end_ts, $config_game_paused) = + await \HH\Asio\va( + Configuration::gen('start_ts'), // Get start time + Configuration::gen('end_ts'), // Get end time + Configuration::gen('game_paused'), // Get paused status + ); $start_ts = intval($config_start_ts->getValue()); - - // Get end time - $config_end_ts = await Configuration::gen('end_ts'); $end_ts = intval($config_end_ts->getValue()); - - // Get paused status - $config_game_paused = await Configuration::gen('game_paused'); $game_paused = intval($config_game_paused->getValue()); if (($game_paused === 0) && ($end_ts <= time())) { - // Start the game - await Control::genEnd(); + await Control::genEnd(); // End the game } } public static async function genAutoRun(): Awaitable { - // Get start time - $config_game = await Configuration::gen('game'); + // Prevent autorun.php from storing timestamps in local cache, forever (the script runs continuously). + Configuration::deleteLocalCache('CONFIGURATION'); + $config_game = await Configuration::gen('game'); // Get start time $game = intval($config_game->getValue()); if ($game === 0) { - // Check and start the game - await Control::genAutoBegin(); + await Control::genAutoBegin(); // Check and start the game } else { - // Check and stop the game - await Control::genAutoEnd(); + await Control::genAutoEnd(); // Check and stop the game } } @@ -318,16 +277,21 @@ class Control extends Model { string $name, ): Awaitable { $db = await self::genDb(); + $host = await Control::genServerAddr(); $result = await $db->queryf( - 'SELECT pid FROM scripts WHERE name = %s AND status = 1 LIMIT 1', + 'SELECT pid FROM scripts WHERE name = %s AND host = %s AND status = 1', $name, + $host, ); + $status = false; if ($result->numRows() >= 1) { - $pid = intval(must_have_idx($result->mapRows()[0], 'pid')); - $status = file_exists("/proc/$pid"); - if ($status === false) { - await Control::genStopScriptLog($pid); - await Control::genClearScriptLog(); + foreach ($result->mapRows() as $row) { + $pid = intval(must_have_idx($row, 'pid')); + $status = file_exists("/proc/$pid"); + if ($status === false) { + await Control::genStopScriptLog($pid); + await Control::genClearScriptLog(); + } } return $status; } else { @@ -464,14 +428,19 @@ class Control extends Model { public static async function exportGame(): Awaitable { $game = array(); - $logos = await Logo::exportAll(); - $game['logos'] = $logos; - $teams = await Team::exportAll(); - $game['teams'] = $teams; - $categories = await Category::exportAll(); - $game['categories'] = $categories; - $levels = await Level::exportAll(); - $game['levels'] = $levels; + $awaitables = Map { + 'logos' => Logo::exportAll(), + 'teams' => Team::exportAll(), + 'categories' => Category::exportAll(), + 'levels' => Level::exportAll(), + }; + $awaitables_results = await \HH\Asio\m($awaitables); + + $game['logos'] = $awaitables['logos']; + $game['teams'] = $awaitables['teams']; + $game['categories'] = $awaitables['categories']; + $game['levels'] = $awaitables['levels']; + $output_file = 'fbctf_game.json'; JSONExporterController::sendJSON($game, $output_file); exit(); @@ -551,8 +520,7 @@ class Control extends Model { } public static async function genFlushMemcached(): Awaitable { - $mc = self::getMc(); - return $mc->flush(0); + return self::flushMCCluster(); } private static async function genLoadDatabaseFile( diff --git a/src/models/Country.php b/src/models/Country.php index 748584b4..75fad5cc 100644 --- a/src/models/Country.php +++ b/src/models/Country.php @@ -55,14 +55,12 @@ public function getTransform(): string { // Make sure all the countries used field is good public static async function genUsedAdjust(): Awaitable { $db = await self::genDb(); - await $db->queryf( + $queries = Vector { 'UPDATE countries SET used = 1 WHERE id IN (SELECT entity_id FROM levels)', - ); - await $db->queryf( 'UPDATE countries SET used = 0 WHERE id NOT IN (SELECT entity_id FROM levels)', - ); - - self::invalidateMCRecords(); // Invalidate Memcached Country data. + }; + await $db->multiQuery($queries); + self::invalidateMCRecords(); } // Enable or disable a country @@ -76,7 +74,7 @@ public function getTransform(): string { $status ? 1 : 0, $country_id, ); - self::invalidateMCRecords(); // Invalidate Memcached Country data. + self::invalidateMCRecords(); } // Set the used flag for a country @@ -90,7 +88,7 @@ public function getTransform(): string { $status ? 1 : 0, $country_id, ); - self::invalidateMCRecords(); // Invalidate Memcached Country data. + self::invalidateMCRecords(); } private static async function genAll( @@ -219,7 +217,7 @@ function($a, $b) { public static async function genIsActiveLevel( int $country_id, ): Awaitable { - return Level::genWhoUses($country_id) != null; + return Level::genWhoUses($country_id) !== null; } // Get a country by id. diff --git a/src/models/Integration.php b/src/models/Integration.php new file mode 100644 index 00000000..1af20e2f --- /dev/null +++ b/src/models/Integration.php @@ -0,0 +1,534 @@ +type; + } + + public static function getIntegrationCacheObject(): Cache { + if (self::$INTEGRATION_CACHE === MUST_MODIFY) { + self::$INTEGRATION_CACHE = new Cache(); + } + invariant( + self::$INTEGRATION_CACHE instanceof Cache, + 'Integration::$INTEGRATION_CACHE should of type Map and not null', + ); + return self::$INTEGRATION_CACHE; + } + + public static async function facebookOAuthEnabled(): Awaitable { + return Configuration::getFacebookOAuthSettingsExists(); + } + + public static async function googleOAuthEnabled(): Awaitable { + return Configuration::getGoogleOAuthFileExists(); + } + + public static async function facebookLoginEnabled(): Awaitable { + $login_facebook = await Configuration::gen('login_facebook'); + $oauth = Configuration::getFacebookOAuthSettingsExists(); + + $login_facebook_enabled = + $login_facebook->getValue() === '1' ? true : false; + + if ($oauth && $login_facebook_enabled) { + return true; + } else { + return false; + } + } + + public static async function googleLoginEnabled(): Awaitable { + $login_google = await Configuration::gen('login_google'); + $oauth = Configuration::getGoogleOAuthFileExists(); + + $login_google_enabled = $login_google->getValue() === '1' ? true : false; + + if (($oauth) && ($login_google_enabled)) { + return true; + } else { + return false; + } + } + + public static async function genFacebookAuthURL( + string $redirect, + bool $rerequest = false, + ): Awaitable<(Facebook, string)> { + $host = strval(idx(Utils::getSERVER(), 'HTTP_HOST')); + $app_id = Configuration::getFacebookOAuthSettingsAppId(); + $app_secret = Configuration::getFacebookOAuthSettingsAppSecret(); + $client = new Facebook\Facebook( + [ + 'app_id' => $app_id, + 'app_secret' => $app_secret, + 'default_graph_version' => 'v2.2', + ], + ); + + $helper = $client->getRedirectLoginHelper(); + + $permissions = ['email']; + $auth_url = $helper->getLoginUrl( + 'https://'.$host.'/data/integration_'.$redirect.'.php?type=facebook', + $permissions, + ); + + if ($rerequest === true) { + $auth_url .= '&auth_type=rerequest'; + } + return tuple($client, $auth_url); + } + + public static async function genFacebookAPIClient( + ): Awaitable<(Facebook, string)> { + $host = strval(idx(Utils::getSERVER(), 'HTTP_HOST')); + $app_id = Configuration::getFacebookOAuthSettingsAppId(); + $app_secret = Configuration::getFacebookOAuthSettingsAppSecret(); + $client = new Facebook\Facebook( + [ + 'app_id' => $app_id, + 'app_secret' => $app_secret, + 'default_graph_version' => 'v2.2', + ], + ); + + $access_token = $app_id.'|'.$app_secret; + + return tuple($client, $access_token); + } + + public static async function genGoogleAuthURL( + string $redirect, + ): Awaitable<(Google_Client, string)> { + $host = strval(idx(Utils::getSERVER(), 'HTTP_HOST')); + $google_oauth_file = Configuration::getGoogleOAuthFile(); + $client = new Google_Client(); + $client->setAuthConfig($google_oauth_file); + $client->setAccessType('offline'); + $client->setScopes(['profile email']); + $client->setRedirectUri( + 'https://'.$host.'/data/integration_'.$redirect.'.php?type=google', + ); + + $integration_csrf_token = bin2hex(random_bytes(100)); + setcookie( + 'integration_csrf_token', + strval($integration_csrf_token), + 0, + '/data/', + must_have_string(Utils::getSERVER(), 'SERVER_NAME'), + true, + true, + ); + $client->setState(strval($integration_csrf_token)); + + $auth_url = $client->createAuthUrl(); + + return tuple($client, $auth_url); + } + + public static async function genFacebookLogin(): Awaitable { + list($client, $url) = await self::genFacebookAuthURL("login"); + $helper = $client->getRedirectLoginHelper(); + + $code = idx(Utils::getGET(), 'code', false); + $error = idx(Utils::getGET(), 'error', false); + + $accessToken = ''; + + if ($code !== false) { + $graph_error = false; + try { + $accessToken = $helper->getAccessToken(); + } catch (Facebook\Exceptions\FacebookResponseException $e) { + $graph_error = true; + } catch (Facebook\Exceptions\FacebookSDKException $e) { + $graph_error = true; + } + + $url = '/index.php?page=login'; + if ($graph_error !== true) { + + $response = false; + try { + $response = + $client->get('/me?fields=id,third_party_id,email', $accessToken); + } catch (Facebook\Exceptions\FacebookResponseException $e) { + error_log("Facebook OAuth Failed - Missing fields"); + $url = '/index.php?page=error'; + return $url; + } catch (Facebook\Exceptions\FacebookSDKException $e) { + error_log("Facebook OAuth Failed - Missing fields"); + $url = '/index.php?page=error'; + return $url; + } + $profile = $response->getGraphUser(); + $email = $profile['email']; + $id = $profile['third_party_id']; + + if ($id === null) { + error_log("Facebook OAuth Failed - Missing id Field"); + list($client, $url) = await self::genFacebookAuthURL("login", true); + return $url; + } + + if ($email === null) { + error_log("Facebook OAuth Failed - Missing email Field - $id"); + list($client, $url) = await self::genFacebookAuthURL("login", true); + return $url; + } + + list($oauth_token_exists, $registration_facebook) = + await \HH\Asio\va( + Team::genAuthTokenExists('facebook_oauth', strval($email)), + Configuration::gen('registration_facebook'), + ); + + if ($oauth_token_exists === true) { + $url = await self::genLoginURL("facebook_oauth", $email); + } else if ($registration_facebook->getValue() === '1') { + $team_id = await self::genRegisterTeam($email, $id); + + if (is_int($team_id) === true) { + $set_integrations = await self::genSetTeamIntegrations( + $team_id, + 'facebook_oauth', + $email, + $id, + ); + if ($set_integrations === true) { + $url = await self::genLoginURL('facebook_oauth', $email); + } + } + } + } + } else if ($error !== false) { + $url = '/index.php?page=login'; + } + + return $url; + } + + public static async function genGoogleLogin(): Awaitable { + list($client, $url) = await self::genGoogleAuthURL("login"); + + $code = idx(Utils::getGET(), 'code', false); + $error = idx(Utils::getGET(), 'error', false); + $state = idx(Utils::getGET(), 'state', false); + + if ($code !== false) { + $integration_csrf_token = /* HH_IGNORE_ERROR[2050] */ + idx($_COOKIE, 'integration_csrf_token', false); + if (strval($integration_csrf_token) === '' || + strval($state) === '' || + strval($integration_csrf_token) !== strval($state)) { + $code = false; + $error = true; + } + } + + if ($code !== false) { + $url = '/index.php?page=login'; + $client->authenticate($code); + $access_token = $client->getAccessToken(); + $oauth_client = new Google_Service_Oauth2($client); + $profile = $oauth_client->userinfo->get(); + $email = $profile->email; + $id = $profile->id; + + list($oauth_token_exists, $registration_google) = await \HH\Asio\va( + Team::genAuthTokenExists('google_oauth', strval($email)), + Configuration::gen('registration_google'), + ); + + if ($oauth_token_exists === true) { + $url = await self::genLoginURL('google_oauth', $email); + } else if ($registration_google->getValue() === '1') { + $team_id = await self::genRegisterTeam($email, $id); + if (is_int($team_id) === true) { + $set_integrations = await self::genSetTeamIntegrations( + $team_id, + 'google_oauth', + $email, + $id, + ); + if ($set_integrations === true) { + $url = await self::genLoginURL('google_oauth', $email); + } + } + } + } else if ($error !== false) { + $url = '/index.php?page=login'; + } + + return $url; + } + + public static async function genLoginURL( + string $type, + string $token, + ): Awaitable { + $team = await Team::genTeamFromOAuthToken($type, $token); + + SessionUtils::sessionRefresh(); + if (!SessionUtils::sessionActive()) { + SessionUtils::sessionSet('team_id', strval($team->getId())); + SessionUtils::sessionSet('name', $team->getName()); + SessionUtils::sessionSet( + 'csrf_token', + (string) strval(bin2hex(random_bytes(100))), + ); + SessionUtils::sessionSet( + 'IP', + must_have_string(Utils::getSERVER(), 'REMOTE_ADDR'), + ); + if ($team->getAdmin()) { + SessionUtils::sessionSet('admin', strval($team->getAdmin())); + } + } + if ($team->getAdmin()) { + $redirect = 'admin'; + } else { + $redirect = 'game'; + } + + $login_url = '/index.php?p='.$redirect; + + return $login_url; + } + + public static async function genFacebookOAuth(): Awaitable { + list($client, $url) = await self::genFacebookAuthURL("oauth"); + $helper = $client->getRedirectLoginHelper(); + + $code = idx(Utils::getGET(), 'code'); + $error = idx(Utils::getGET(), 'error'); + + if (!is_string($code)) { + $code = false; + } + + if (!is_string($error)) { + $error = false; + } + + $accessToken = ''; + + if ($code !== false) { + $graph_error = false; + try { + $accessToken = $helper->getAccessToken(); + } catch (Facebook\Exceptions\FacebookResponseException $e) { + $graph_error = true; + } catch (Facebook\Exceptions\FacebookSDKException $e) { + $graph_error = true; + } + + if ($graph_error === true) { + return false; + } else { + $response = + $client->get('/me?fields=id,third_party_id,email', $accessToken); + $profile = $response->getGraphUser(); + $email = $profile['email']; + $id = $profile['third_party_id']; + + if ($email === null) { + list($client, $url) = await self::genFacebookAuthURL("oauth", true); + header('Location: '.filter_var($url, FILTER_SANITIZE_URL)); + exit; + } + + $set_integrations = await self::genSetTeamIntegrations( + SessionUtils::sessionTeam(), + 'facebook_oauth', + $email, + $id, + ); + return $set_integrations; + } + } else if ($error !== false) { + return false; + } + + header('Location: '.filter_var($url, FILTER_SANITIZE_URL)); + exit; + return false; + } + + public static async function genGoogleOAuth(): Awaitable { + list($client, $url) = await self::genGoogleAuthURL("oauth"); + + $code = idx(Utils::getGET(), 'code', false); + $error = idx(Utils::getGET(), 'error', false); + $state = idx(Utils::getGET(), 'state', false); + + if ($code !== false) { + $integration_csrf_token = /* HH_IGNORE_ERROR[2050] */ + idx($_COOKIE, 'integration_csrf_token', false); + if (strval($integration_csrf_token) === '' || + strval($state) === '' || + strval($integration_csrf_token) !== strval($state)) { + $code = false; + $error = true; + } + } + + if ($code !== false) { + $client->authenticate($code); + $access_token = $client->getAccessToken(); + $oauth_client = new Google_Service_Oauth2($client); + $profile = $oauth_client->userinfo->get(); + $email = $profile->email; + $id = $profile->id; + + $set_integrations = await self::genSetTeamIntegrations( + SessionUtils::sessionTeam(), + 'google_oauth', + $email, + $id, + ); + return $set_integrations; + } else if ($error !== false) { + return false; + } + + header('Location: '.filter_var($url, FILTER_SANITIZE_URL)); + exit; + return false; + } + + public static async function genSetTeamIntegrations( + int $team_id, + string $type, + string $email, + string $id, + ): Awaitable { + list($livesync_password_update, $oauth_token_update) = await \HH\Asio\va( + Team::genSetLiveSyncPassword($team_id, $type, $email, $id), + Team::genSetOAuthToken($team_id, $type, $email), + ); + + if (($livesync_password_update === true) && + ($oauth_token_update === true)) { + return true; + } else { + return false; + } + } + + public static async function genRegisterTeam( + string $email, + string $id, + string $name = '', + ): Awaitable { + list($registration_prefix, $logo_name) = await \HH\Asio\va( + Configuration::gen('registration_prefix'), + Logo::genRandomLogo(), + ); + + $team_password = Team::generateHash(random_bytes(100)); + $team_name = substr( + substr($registration_prefix->getValue(), 0, 14). + "-". + bin2hex(random_bytes(12)), + 0, + 20, + ); + if ($name !== '') { + $team_name = substr(strval($name), 0, 20); + } + $team_id = await Team::genCreate($team_name, $team_password, $logo_name); + return $team_id; + } + + public static async function genFacebookThirdPartyExists( + string $third_party_id, + ): Awaitable { + self::getIntegrationCacheObject(); + if (self::$INTEGRATION_CACHE->getCache( + 'facebook_exists:'.$third_party_id, + ) === + true) { + return true; + } else { + list($client, $access_token) = await self::genFacebookAPIClient(); + + try { + $response = + $client->get('/'.$third_party_id.'?fields=email', $access_token); + } catch (FacebookExceptionsFacebookResponseException $e) { + print "error 1\n\n\n"; + return false; + } catch (FacebookExceptionsFacebookSDKException $e) { + print "error 2\n\n\n"; + return false; + } + $graphNode = $response->getGraphNode(); + $graphNode->getField('email'); + $profile = $response->getGraphUser(); + $email = strval($profile->getEmail()); + if ($email !== null) { + self::$INTEGRATION_CACHE->setCache( + 'facebook_exists:'.$third_party_id, + true, + ); + self::$INTEGRATION_CACHE->setCache( + 'facebook_email:'.$third_party_id, + $email, + ); + return true; + } else { + return false; + } + } + } + + public static async function genFacebookThirdPartyEmail( + string $third_party_id, + ): Awaitable { + self::getIntegrationCacheObject(); + $integration_local_cache = + self::$INTEGRATION_CACHE->getCache('facebook_email:'.$third_party_id); + if ($integration_local_cache !== false) { + return strval($integration_local_cache); + } else { + list($client, $access_token) = await self::genFacebookAPIClient(); + + try { + $response = + $client->get('/'.$third_party_id.'?fields=email', $access_token); + } catch (FacebookExceptionsFacebookResponseException $e) { + return ''; + } catch (FacebookExceptionsFacebookSDKException $e) { + return ''; + } + $graphNode = $response->getGraphNode(); + $graphNode->getField('email'); + $profile = $response->getGraphUser(); + $email = $profile->getEmail(); + if ($email !== null) { + self::$INTEGRATION_CACHE->setCache( + 'facebook_exists:'.$third_party_id, + true, + ); + self::$INTEGRATION_CACHE->setCache( + 'facebook_email:'.$third_party_id, + $email, + ); + return $email; + } else { + return ''; + } + } + } + +} diff --git a/src/models/Level.php b/src/models/Level.php index 0a10fd60..e559621b 100644 --- a/src/models/Level.php +++ b/src/models/Level.php @@ -9,6 +9,7 @@ class Level extends Model implements Importable, Exportable { 'LEVEL_BY_COUNTRY' => 'level_by_country', 'ALL_LEVELS' => 'all_levels', 'ALL_ACTIVE_LEVELS' => 'active_levels', + 'ALL_LEVELS_COUNTRY_MAP' => 'all_levels_country_map', }; private function __construct( @@ -42,11 +43,11 @@ public function getType(): string { } public function getTitle(): string { - return $this->title; + return mb_convert_encoding($this->title, 'UTF-8'); } public function getDescription(): string { - return $this->description; + return mb_convert_encoding($this->description, 'UTF-8'); } public function getEntityId(): int { @@ -153,11 +154,15 @@ private static function levelFromRow(Map $row): Level { $entity_iso_code = must_have_string($level, 'entity_iso_code'); $c = must_have_string($level, 'category'); $exist = await self::genAlreadyExist($type, $title, $entity_iso_code); - $entity_exist = await Country::genCheckExists($entity_iso_code); - $category_exist = await Category::genCheckExists($c); + list($entity_exist, $category_exist) = await \HH\Asio\va( + Country::genCheckExists($entity_iso_code), + Category::genCheckExists($c), + ); // TODO: Combine Awaits if (!$exist && $entity_exist && $category_exist) { - $entity = await Country::genCountry($entity_iso_code); - $category = await Category::genSingleCategoryByName($c); + list($entity, $category) = await \HH\Asio\va( + Country::genCountry($entity_iso_code), + Category::genSingleCategoryByName($c), + ); // TODO: Combine Awaits $level_id = await self::genCreate( $type, $title, @@ -176,7 +181,7 @@ private static function levelFromRow(Map $row): Level { $links = must_have_idx($level, 'links'); invariant(is_array($links), 'links must be of type array'); foreach ($links as $link) { - await Link::genCreate($link, $level_id); + await Link::genCreate($link, $level_id); // TODO: Combine Awaits } } if (array_key_exists('attachments', $level)) { @@ -190,7 +195,7 @@ private static function levelFromRow(Map $row): Level { $level_id, $attachment['filename'], $attachment['type'], - ); + ); // TODO: Combine Awaits } } } @@ -205,14 +210,17 @@ private static function levelFromRow(Map $row): Level { $all_levels = await self::genAllLevels(); foreach ($all_levels as $level) { - $entity = await Country::gen($level->getEntityId()); - $category = await Category::genSingleCategory($level->getCategoryId()); - $links = await Link::genAllLinks($level->getId()); + list($entity, $category, $links, $attachments) = await \HH\Asio\va( + Country::gen($level->getEntityId()), + Category::genSingleCategory($level->getCategoryId()), + Link::genAllLinks($level->getId()), + Attachment::genAllAttachments($level->getId()), + ); // TODO: Combine Awaits + $link_array = array(); foreach ($links as $link) { $link_array[] = $link->getLink(); } - $attachments = await Attachment::genAllAttachments($level->getId()); $attachment_array = array(); foreach ($attachments as $attachment) { $attachment_array[] = [ @@ -381,16 +389,19 @@ private static function levelFromRow(Map $row): Level { $category_id, ); - self::invalidateMCRecords(); // Invalidate Memcached Level data. + self::invalidateMCRecords(); invariant($result->numRows() === 1, 'Expected exactly one result'); $country_id = await self::genCountryIdForLevel( intval(must_have_idx($result->mapRows()[0], 'id')), ); - await ActivityLog::genAdminLog("added", "Country", $country_id); $country = await Country::gen($country_id); - await Announcement::genCreateAuto($country->getName()." added!"); - ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. + await \HH\Asio\va( + Announcement::genCreateAuto($country->getName()." added!"), + ActivityLog::genAdminLog("added", "Country", $country_id), + ); + + ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); return intval(must_have_idx($result->mapRows()[0], 'id')); } @@ -629,9 +640,11 @@ private static function levelFromRow(Map $row): Level { if ($result->numRowsAffected() > 0) { $country_id = await self::genCountryIdForLevel($level_id); - await ActivityLog::genAdminLog("updated", "Country", $country_id); $country = await Country::gen($country_id); - await Announcement::genCreateAuto($country->getName()." updated!"); + await \HH\Asio\va( + ActivityLog::genAdminLog("updated", "Country", $country_id), + Announcement::genCreateAuto($country->getName()." updated!"), + ); self::invalidateMCRecords(); // Invalidate Memcached Level data. ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. } @@ -708,11 +721,13 @@ private static function levelFromRow(Map $row): Level { if ($result->numRowsAffected() > 0) { $action = ($active === true) ? "enabled" : "disabled"; $country_id = await self::genCountryIdForLevel($level_id); - await ActivityLog::genAdminLog($action, "Country", $country_id); $country = await Country::gen($country_id); - await Announcement::genCreateAuto($country->getName().' '.$action.'!'); - self::invalidateMCRecords(); // Invalidate Memcached Level data. - ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. + await \HH\Asio\va( + ActivityLog::genAdminLog($action, "Country", $country_id), + Announcement::genCreateAuto($country->getName().' '.$action.'!'), + ); + self::invalidateMCRecords(); + ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); } } @@ -730,7 +745,7 @@ private static function levelFromRow(Map $row): Level { ); if ($results->numRowsAffected() > 0) { - self::invalidateMCRecords(); // Invalidate Memcached Level data. + self::invalidateMCRecords(); } } @@ -754,7 +769,7 @@ private static function levelFromRow(Map $row): Level { ); } foreach ($result->mapRows() as $row) { - await self::genSetStatus(intval($row->get('id')), $active); + await self::genSetStatus(intval($row->get('id')), $active); // TODO: Combine Awaits } } @@ -787,6 +802,31 @@ private static function levelFromRow(Map $row): Level { } } + public static async function genAllLevelsCountryMap( + bool $refresh = false, + ): Awaitable> { + $mc_result = self::getMCRecords('ALL_LEVELS_COUNTRY_MAP'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $db = await self::genDb(); + $all_levels = Map {}; + $result = await $db->queryf('SELECT * FROM levels ORDER BY id'); + foreach ($result->mapRows() as $row) { + $all_levels->add( + Pair {intval($row->get('entity_id')), self::levelFromRow($row)}, + ); + } + self::setMCRecords('ALL_LEVELS_COUNTRY_MAP', new Map($all_levels)); + return $all_levels; + } else { + $levels = array(); + invariant( + $mc_result instanceof Map, + 'cache return should be of type Map', + ); + return $mc_result; + } + } + // All levels by status. public static async function genAllActiveLevels( bool $refresh = false, @@ -1043,7 +1083,7 @@ private static function levelFromRow(Map $row): Level { // Check if team has already scored this level $previous_score = - await ScoreLog::genPreviousScore($level_id, $team_id, false); + await ScoreLog::genAllPreviousScore($level_id, $team_id, false); if ($previous_score) { return false; } @@ -1053,24 +1093,26 @@ private static function levelFromRow(Map $row): Level { // Calculate points to give $points = $level->getPoints() + $level->getBonus(); - // Adjust bonus - await self::genAdjustBonus($level_id); - - // Score! - await $db->queryf( - 'UPDATE teams SET points = points + %d, last_score = NOW() WHERE id = %d LIMIT 1', - $points, - $team_id, - ); - // Log the score - await ScoreLog::genLogValidScore( + $captured = await ScoreLog::genLogValidScore( $level_id, $team_id, $points, $level->getType(), ); + if ($captured === true) { + // Adjust bonus + await self::genAdjustBonus($level_id); + + // Score! + await $db->queryf( + 'UPDATE teams SET points = points + %d, last_score = NOW() WHERE id = %d LIMIT 1', + $points, + $team_id, + ); + } + self::invalidateMCRecords(); // Invalidate Memcached Level data. return true; @@ -1096,7 +1138,7 @@ private static function levelFromRow(Map $row): Level { // Calculate points to give $score = - await ScoreLog::genPreviousScore($level_id, $team_id, false); + await ScoreLog::genAllPreviousScore($level_id, $team_id, false); if ($score) { $points = $level->getPoints(); } else { @@ -1118,7 +1160,7 @@ private static function levelFromRow(Map $row): Level { $level->getType(), ); - self::invalidateMCRecords(); // Invalidate Memcached Level data. + self::invalidateMCRecords(); return true; }, @@ -1144,9 +1186,10 @@ private static function levelFromRow(Map $row): Level { // Check if team has already gotten this hint or if the team has scored this already // If so, hint is free - $hint = await HintLog::genPreviousHint($level_id, $team_id, false); - $score = - await ScoreLog::genPreviousScore($level_id, $team_id, false); + list($hint, $score) = await \HH\Asio\va( + HintLog::genPreviousHint($level_id, $team_id, false), + ScoreLog::genAllPreviousScore($level_id, $team_id, false), + ); if ($hint || $score) { $penalty = 0; } @@ -1157,22 +1200,25 @@ private static function levelFromRow(Map $row): Level { return null; } - // Adjust points - await $db->queryf( - 'UPDATE teams SET points = points - %d WHERE id = %d LIMIT 1', - $penalty, - $team_id, + // Adjust points and log the hint + await \HH\Asio\va( + $db->queryf( + 'UPDATE teams SET points = points - %d WHERE id = %d LIMIT 1', + $penalty, + $team_id, + ), + HintLog::genLogGetHint($level_id, $team_id, $penalty), ); - // Log the hint - await HintLog::genLogGetHint($level_id, $team_id, $penalty); - ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. MultiTeam::invalidateMCRecords('ALL_TEAMS'); // Invalidate Memcached MultiTeam data. MultiTeam::invalidateMCRecords('POINTS_BY_TYPE'); // Invalidate Memcached MultiTeam data. MultiTeam::invalidateMCRecords('LEADERBOARD'); // Invalidate Memcached MultiTeam data. + $completed_level = await MultiTeam::genCompletedLevel($level_id); + if (count($completed_level) === 0) { + MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. + } MultiTeam::invalidateMCRecords('TEAMS_BY_LEVEL'); // Invalidate Memcached MultiTeam data. - MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. // Hint! return $level->getHint(); diff --git a/src/models/Link.php b/src/models/Link.php index ac30680e..9c514c6c 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -9,6 +9,7 @@ class Link extends Model { 'LEVELS_COUNT' => 'link_levels_count', 'LEVEL_LINKS' => 'link_levels', 'LINKS' => 'link_by_id', + 'LEVEL_LINKS_VALUES' => 'link_level_values', }; private function __construct( @@ -26,7 +27,7 @@ public function getLevelId(): int { } public function getLink(): string { - return $this->link; + return mb_convert_encoding($this->link, 'UTF-8'); } // Create link for a given level. @@ -106,6 +107,75 @@ public function getLink(): string { } } + public static async function genAllLinksForGame( + bool $refresh = false, + ): Awaitable> { + $mc_result = self::getMCRecords('LEVEL_LINKS'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $db = await self::genDb(); + $links = array(); + $result = await $db->queryf('SELECT * FROM links'); + foreach ($result->mapRows() as $row) { + $links[intval($row->get('level_id'))][] = self::linkFromRow($row); + } + self::setMCRecords('LEVEL_LINKS', new Map($links)); + $links = new Map($links); + invariant($links instanceof Map, 'links should be a Map of Link'); + return $links; + } else { + invariant($mc_result instanceof Map, 'links should be of type Map'); + invariant($mc_result instanceof Map, 'cache should be a Map of Link'); + return $mc_result; + } + } + + public static async function genAllLinksValues( + int $level_id, + bool $refresh = false, + ): Awaitable> { + $mc_result = self::getMCRecords('LEVEL_LINKS_VALUES'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $db = await self::genDb(); + $link_values = array(); + $links = await self::genAllLinksForGame(); + invariant($links instanceof Map, 'link should be a Map of Link'); + foreach ($links as $level => $link_arr) { + invariant(is_array($link_arr), 'link_arr should be an array of Link'); + foreach ($link_arr as $link_obj) { + invariant( + $link_obj instanceof Link, + 'link_obj should be of type Link', + ); + $link_values[$level][] = $link_obj->getLink(); + } + } + self::setMCRecords('LEVEL_LINKS_VALUES', new Map($link_values)); + $link_values = new Map($link_values); + if ($link_values->contains($level_id)) { + $link_array = $link_values->get($level_id); + invariant( + is_array($link_array), + 'link_array should be an array of string', + ); + return $link_array; + } else { + return array(); + } + } else { + invariant($mc_result instanceof Map, 'links should be of type Map'); + if ($mc_result->contains($level_id)) { + $link_array = $mc_result->get($level_id); + invariant( + is_array($link_array), + 'link_array should be an array of string', + ); + return $link_array; + } else { + return array(); + } + } + } + // Get a single link. public static async function gen( int $link_id, diff --git a/src/models/Logo.php b/src/models/Logo.php index 0d28726a..633be8ec 100644 --- a/src/models/Logo.php +++ b/src/models/Logo.php @@ -164,11 +164,20 @@ public function getCustom(): bool { await $db->queryf( 'SELECT * FROM logos WHERE enabled = 1 AND used = 0 AND protected = 0 AND custom = 0', ); - - foreach ($result->mapRows() as $row) { - $all_enabled_logos[] = self::logoFromRow($row); + // If all logos are used, provide all logos with no restriction on unused. + if ($result->numRows() === 0) { + $all_logos = await self::genAllLogos(); + $all_enabled_logos = $all_logos->toArray(); + } else { + foreach ($result->mapRows() as $row) { + $all_enabled_logos[] = self::logoFromRow($row); + } } self::setMCRecords('ALL_ENABLED_LOGOS', $all_enabled_logos); + invariant( + is_array($all_enabled_logos), + 'all_enabled_logos should be an array of Logo', + ); return $all_enabled_logos; } else { invariant( @@ -197,7 +206,7 @@ private static function logoFromRow(Map $row): Logo { ): Awaitable { foreach ($elements as $logo) { $name = must_have_string($logo, 'name'); - $exist = await self::genCheckExists($name); + $exist = await self::genCheckExists($name); // TODO: Combine Awaits if (!$exist) { await self::genCreate( (bool) must_have_idx($logo, 'used'), @@ -206,7 +215,7 @@ private static function logoFromRow(Map $row): Logo { (bool) must_have_idx($logo, 'custom'), $name, must_have_string($logo, 'logo'), - ); + ); // TODO: Combine Awaits } } return true; diff --git a/src/models/Model.php b/src/models/Model.php index 2e096e24..b8a808b4 100644 --- a/src/models/Model.php +++ b/src/models/Model.php @@ -1,11 +1,16 @@ $MC_KEYS = Map {}; protected static async function genDb(): Awaitable { @@ -21,37 +26,143 @@ abstract class Model { protected static function getMc(): Memcached { if (self::$mc === MUST_MODIFY) { $config = parse_ini_file('../../settings.ini'); - $host = must_have_idx($config, 'MC_HOST'); + $cluster = must_have_idx($config, 'MC_HOST'); $port = must_have_idx($config, 'MC_PORT'); + $host = $cluster[array_rand($cluster)]; self::$mc = new Memcached(); self::$mc->addServer($host, $port); } return self::$mc; } + public static function getMemcachedStats(): mixed { + $stats = array(); + $mc = self::getMcWrite(); + foreach ($mc->getServerList() as $node) { + $mc_node = new Memcached(); + $mc_node->addServer($node['host'], $node['port']); + $stats[$node['host']] = $mc_node->getStats(); + } + return $stats; + } + + /** + * @codeCoverageIgnore + */ + protected static function getMcWrite(): Memcached { + if (self::$mc_write === MUST_MODIFY) { + $config = parse_ini_file('../../settings.ini'); + $cluster = must_have_idx($config, 'MC_HOST'); + $port = must_have_idx($config, 'MC_PORT'); + self::$mc_write = new Memcached(); + foreach ($cluster as $node) { + self::$mc_write->addServer($node, $port); + } + } + return self::$mc_write; + } + protected static function setMCRecords(string $key, mixed $records): void { - $mc = self::getMc(); - $mc->set( - static::$MC_KEY.static::$MC_KEYS->get($key), - $records, - static::$MC_EXPIRE, - ); + self::getCacheClassObject(); + $cache_key = static::$MC_KEY.static::$MC_KEYS->get($key); + + self::writeMCCluster($cache_key, $records); + self::$CACHE->setCache($cache_key, $records); } protected static function getMCRecords(string $key): mixed { - $mc = self::getMc(); - $mc_result = $mc->get(static::$MC_KEY.static::$MC_KEYS->get($key)); - return $mc_result; + self::getCacheClassObject(); + $cache_key = static::$MC_KEY.static::$MC_KEYS->get($key); + + $local_cache_result = self::$CACHE->getCache($cache_key); + if ($local_cache_result !== false) { + return $local_cache_result; + } else { + $mc = self::getMc(); + $mc_result = $mc->get($cache_key); + if ($mc_result !== false) { + self::$CACHE->setCache($cache_key, $mc_result); + } + + return $mc_result; + } } public static function invalidateMCRecords(?string $key = null): void { - $mc = self::getMc(); + self::getCacheClassObject(); + if ($key === null) { foreach (static::$MC_KEYS as $key_name => $mc_key) { - $mc->delete(static::$MC_KEY.static::$MC_KEYS->get($key_name)); + $cache_key = static::$MC_KEY.static::$MC_KEYS->get($key_name); + self::invalidateMCCluster($cache_key); + self::$CACHE->deleteCache($cache_key); + } + } else { + $cache_key = static::$MC_KEY.static::$MC_KEYS->get($key); + self::invalidateMCCluster($cache_key); + self::$CACHE->deleteCache($cache_key); + } + } + + public static function flushMCCluster(): bool { + $mc = self::getMcWrite(); + $status = false; + foreach ($mc->getServerList() as $node) { + $mc_node = new Memcached(); + $mc_node->addServer($node['host'], $node['port']); + $flush_status = $mc_node->flush(0); + if ($flush_status === true) { + $status = true; + } + } + return $status; + } + + protected static function writeMCCluster( + string $cache_key, + mixed $records, + ): void { + $mc = self::getMcWrite(); + foreach ($mc->getServerList() as $node) { + $mc_node = new Memcached(); + $mc_node->addServer($node['host'], $node['port']); + $mc_node->set($cache_key, $records, static::$MC_EXPIRE); + } + } + + protected static function invalidateMCCluster(string $cache_key): void { + $mc = self::getMcWrite(); + foreach ($mc->getServerList() as $node) { + $mc_node = new Memcached(); + $mc_node->addServer($node['host'], $node['port']); + $mc_node->delete($cache_key); + } + } + + public static function getCacheClassObject(): Cache { + if (self::$CACHE === MUST_MODIFY) { + self::$CACHE = new Cache(); + } + invariant( + self::$CACHE instanceof Cache, + 'Model::$CACHE should of type Map and not null', + ); + return self::$CACHE; + } + + public static function deleteLocalCache(?string $key = null): void { + self::getCacheClassObject(); + + if (get_called_class() === 'Model') { + self::$CACHE->flushCache(); + } else if ($key === null) { + foreach (static::$MC_KEYS as $key_name => $mc_key) { + $cache_key = static::$MC_KEY.static::$MC_KEYS->get($key_name); + self::$CACHE->deleteCache($cache_key); } } else { - $mc->delete(static::$MC_KEY.static::$MC_KEYS->get($key)); + $cache_key = static::$MC_KEY.static::$MC_KEYS->get($key); + self::$CACHE->deleteCache($cache_key); } } } diff --git a/src/models/MultiTeam.php b/src/models/MultiTeam.php index 7c9c8929..862b5b5b 100644 --- a/src/models/MultiTeam.php +++ b/src/models/MultiTeam.php @@ -8,18 +8,25 @@ class MultiTeam extends Team { $MC_KEYS = Map { 'ALL_TEAMS' => 'all_teams', 'LEADERBOARD' => 'leaderboard_teams', + 'LEADERBOARD_LIMIT' => 'leaderboard_limit', 'POINTS_BY_TYPE' => 'points_by_type', 'ALL_ACTIVE_TEAMS' => 'active_teams', 'ALL_VISIBLE_TEAMS' => 'visible_teams', 'TEAMS_BY_LOGO' => 'logo_teams', 'TEAMS_BY_LEVEL' => 'level_teams', + 'TEAMS_NAMES_BY_LEVEL' => 'level_teams_names', 'TEAMS_FIRST_CAP' => 'capture_teams', }; private static async function genTeamArrayFromDB( string $query, + int $limit = 0, ): Awaitable>> { $db = await self::genDb(); + if ($limit !== 0) { + $query .= ' LIMIT '.intval($limit); + } + $result = await $db->query($query); return $result->mapRows(); @@ -66,24 +73,57 @@ class MultiTeam extends Team { ): Awaitable { $all_teams = await self::genAllTeamsCache($refresh); $team = $all_teams->get($team_id); - invariant( - $team instanceof Team, - 'all_teams should of type Team and not null', - ); + invariant($team instanceof Team, 'team should of type Team and not null'); return $team; } + public static async function genLeaderboardLimit(): Awaitable { + $limit = false; + $limit = self::getMCRecords('LEADERBOARD_LIMIT'); + if (!$limit) { + $limit = await self::setLeaderboardLimit(); + } + return intval($limit); + } + + public static async function setLeaderboardLimit(): Awaitable { + $leaderboard_limit = await Configuration::gen('leaderboard_limit'); + self::setMCRecords('LEADERBOARD_LIMIT', $leaderboard_limit->getValue()); + return intval($leaderboard_limit->getValue()); + } + // Leaderboard order. public static async function genLeaderboard( + bool $limit = true, bool $refresh = false, ): Awaitable> { + list($leaderboard_limit, $leaderboard_limit_cache, $visible_teams) = + await \HH\Asio\va( + Configuration::gen('leaderboard_limit'), + self::genLeaderboardLimit(), + self::genAllVisibleTeams(), + ); + + $teams_count = count($visible_teams); $mc_result = self::getMCRecords('LEADERBOARD'); - if (!$mc_result || count($mc_result) === 0 || $refresh) { + if (!$mc_result || + count($mc_result) === 0 || + $leaderboard_limit_cache !== intval($leaderboard_limit->getValue()) || + ($limit === false && (count($mc_result) !== $teams_count)) || + $refresh) { + if ($limit === true) { + $teams = + await self::genTeamArrayFromDB( + 'SELECT * FROM teams WHERE active = 1 AND visible = 1 ORDER BY points DESC, last_score ASC', + intval($leaderboard_limit->getValue()), + ); + } else { + $teams = + await self::genTeamArrayFromDB( + 'SELECT * FROM teams WHERE active = 1 AND visible = 1 ORDER BY points DESC, last_score ASC', + ); + } $team_leaderboard = array(); - $teams = - await self::genTeamArrayFromDB( - 'SELECT * FROM teams WHERE active = 1 AND visible = 1 ORDER BY points DESC, last_score ASC', - ); foreach ($teams->items() as $team) { $team_leaderboard[] = Team::teamFromRow($team); } @@ -274,13 +314,25 @@ class MultiTeam extends Team { $mc_result = self::getMCRecords('TEAMS_BY_LEVEL'); if (!$mc_result || count($mc_result) === 0 || $refresh) { $teams_by_completed_level = array(); - $teams = + $scores = await self::genTeamArrayFromDB( - 'SELECT scores_log.level_id, teams.* FROM teams LEFT JOIN scores_log ON teams.id = scores_log.team_id WHERE teams.visible = 1 AND teams.active = 1 AND level_id IS NOT NULL ORDER BY scores_log.ts', + 'SELECT level_id, team_id FROM scores_log WHERE level_id IS NOT NULL ORDER BY ts', ); - foreach ($teams->items() as $team) { - $teams_by_completed_level[intval($team->get('level_id'))][] = - Team::teamFromRow($team); + $team_scores_awaitables = Map {}; + foreach ($scores->items() as $score) { + $team_scores_awaitables->add( + Pair { + $score->get('level_id'), + self::genTeam(intval($score->get('team_id'))), + }, + ); + } + $team_scores = await \HH\Asio\m($team_scores_awaitables); + + foreach ($team_scores as $level_id_key => $team) { + if ($team->getActive() === true && $team->getVisible() === true) { + $teams_by_completed_level[intval($level_id_key)][] = $team; + } } self::setMCRecords( 'TEAMS_BY_LEVEL', @@ -315,6 +367,127 @@ class MultiTeam extends Team { } } + public static async function genAllCompletedLevels( + bool $refresh = false, + ): Awaitable> { + $mc_result = self::getMCRecords('TEAMS_BY_LEVEL'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $teams_by_completed_level = array(); + $scores = + await self::genTeamArrayFromDB( + 'SELECT level_id, team_id FROM scores_log WHERE level_id IS NOT NULL ORDER BY ts', + ); + $team_scores_awaitables = Map {}; + foreach ($scores->items() as $score) { + if ($team_scores_awaitables->contains( + intval($score->get('level_id')), + )) { + $teams_vector = + $team_scores_awaitables->get(intval($score->get('level_id'))); + invariant( + $teams_vector instanceof Vector, + 'teams_map should of type Vector and not null', + ); + $teams_vector->add(self::genTeam(intval($score->get('team_id')))); + $team_scores_awaitables->set( + intval($score->get('level_id')), + $teams_vector, + ); + } else { + $teams_vector = Vector {}; + $teams_vector->add(self::genTeam(intval($score->get('team_id')))); + $team_scores_awaitables->add( + Pair {intval($score->get('level_id')), $teams_vector}, + ); + } + } + + $team_scores = await \HH\Asio\mmk( + $team_scores_awaitables, + async ($level_id_map_key, $teams_map) ==> { + $teams = await \HH\Asio\v($teams_map); + return $teams; + }, + ); + + foreach ($team_scores as $level_id_key => $teams_vector) { + foreach ($teams_vector as $team) { + if ($team->getActive() === true && $team->getVisible() === true) { + $teams_by_completed_level[intval($level_id_key)][] = $team; + } + } + } + self::setMCRecords( + 'TEAMS_BY_LEVEL', + new Map($teams_by_completed_level), + ); + $teams_by_completed_level = new Map($teams_by_completed_level); + invariant( + $teams_by_completed_level instanceof Map, + 'teams_by_completed_level should be a Map of Team', + ); + return $teams_by_completed_level; + } else { + invariant( + $mc_result instanceof Map, + 'cache return should of type Map and not null', + ); + return $mc_result; + } + } + + public static async function genCompletedLevelTeamNames( + int $level_id, + bool $refresh = false, + ): Awaitable> { + $mc_result = self::getMCRecords('TEAMS_NAMES_BY_LEVEL'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $team_names = array(); + $teams = await self::genAllCompletedLevels(); + invariant($teams instanceof Map, 'teams should be a Map of Team'); + foreach ($teams as $level => $completed_arr) { + invariant( + is_array($completed_arr), + 'completed_arr should be an array of Team', + ); + foreach ($completed_arr as $team_obj) { + invariant( + $team_obj instanceof Team, + 'team_obj should be of type Team', + ); + $team_names[$level][] = $team_obj->getName(); + } + } + self::setMCRecords('TEAMS_NAMES_BY_LEVEL', new Map($team_names)); + $team_names = new Map($team_names); + if ($team_names->contains($level_id)) { + $team_name = $team_names->get($level_id); + invariant( + is_array($team_name), + 'team_name should be an array of string and not null', + ); + return $team_name; + } else { + return array(); + } + } else { + invariant( + $mc_result instanceof Map, + 'cache return should of type string and not null', + ); + if ($mc_result->contains($level_id)) { + $team_name = $mc_result->get($level_id); + invariant( + is_array($team_name), + 'cache return should be an array of string and not null', + ); + return $team_name; + } else { + return array(); + } + } + } + public static async function genFirstCapture( int $level_id, bool $refresh = false, @@ -322,13 +495,23 @@ class MultiTeam extends Team { $mc_result = self::getMCRecords('TEAMS_FIRST_CAP'); if (!$mc_result || count($mc_result) === 0 || $refresh) { $first_team_captured_by_level = array(); - $teams = + $captures = await self::genTeamArrayFromDB( - 'SELECT * FROM teams LEFT JOIN scores_log ON teams.id = scores_log.team_id WHERE scores_log.ts IN (SELECT MIN(scores_log.ts) FROM scores_log WHERE scores_log.team_id IN (SELECT id FROM teams) GROUP BY scores_log.level_id)', + 'SELECT sl.level_id, sl.team_id FROM (SELECT level_id, MIN(ts) ts FROM scores_log LEFT JOIN teams ON team_id = teams.id WHERE teams.visible = 1 AND teams.active = 1 GROUP BY level_id) sl2 JOIN scores_log sl ON sl.level_id = sl2.level_id AND sl.ts = sl2.ts;', ); - foreach ($teams->items() as $team) { - $first_team_captured_by_level[intval($team->get('level_id'))] = - Team::teamFromRow($team); + $team_scores_awaitables = Map {}; + foreach ($captures->items() as $capture) { + $team_scores_awaitables->add( + Pair { + $capture->get('level_id'), + self::genTeam(intval($capture->get('team_id'))), + }, + ); + } + $team_scores = await \HH\Asio\m($team_scores_awaitables); + + foreach ($team_scores as $level_id_key => $team) { + $first_team_captured_by_level[intval($level_id_key)] = $team; } self::setMCRecords( 'TEAMS_FIRST_CAP', @@ -354,4 +537,26 @@ class MultiTeam extends Team { return $team; } } + + public static async function genMyTeamRank( + int $team_id, + ): Awaitable<(Team, int)> { + $team = false; + $rank = 1; + $leaderboard = await MultiTeam::genLeaderboard(); + foreach ($leaderboard as $team) { + if ($team_id === $team->getId()) { + return tuple($team, $rank); + } + $rank++; + } + + invariant( + $team instanceof Team, + 'team return should of type Team and not null', + ); + + return tuple($team, $rank); + } + } diff --git a/src/models/ScoreLog.php b/src/models/ScoreLog.php index 1c534847..d4813900 100644 --- a/src/models/ScoreLog.php +++ b/src/models/ScoreLog.php @@ -5,7 +5,12 @@ class ScoreLog extends Model { protected static string $MC_KEY = 'scorelog:'; protected static Map - $MC_KEYS = Map {'LEVEL_CAPTURES' => 'capture_teams'}; + $MC_KEYS = Map { + 'LEVEL_CAPTURES' => 'capture_teams', + 'ALL_SCORES' => 'all_scores', + 'SCORES_BY_TEAM' => 'scores_by_team', + 'ALL_LEVEL_CAPTURES' => 'all_capture_teams', + }; private function __construct( private int $id, @@ -54,14 +59,25 @@ private static function scorelogFromRow(Map $row): ScoreLog { // Get all scores. public static async function genAllScores(): Awaitable> { $db = await self::genDb(); - $result = await $db->queryf('SELECT * FROM scores_log ORDER BY ts DESC'); + $mc_result = self::getMCRecords('ALL_SCORES'); + if (!$mc_result || count($mc_result) === 0) { + $result = + await $db->queryf('SELECT * FROM scores_log ORDER BY ts DESC'); - $scores = array(); - foreach ($result->mapRows() as $row) { - $scores[] = self::scorelogFromRow($row); - } + $scores = array(); + foreach ($result->mapRows() as $row) { + $scores[] = self::scorelogFromRow($row); + } - return $scores; + self::setMCRecords('ALL_SCORES', $scores); + return $scores; + } else { + invariant( + is_array($mc_result), + 'cache return should be an array of type ScoreLog and not null', + ); + return $mc_result; + } } // Reset all scores. @@ -77,7 +93,7 @@ private static function scorelogFromRow(Map $row): ScoreLog { MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. } - // Check if there is a previous score. + // Check if there is a previous score. - honors team visibility public static async function genPreviousScore( int $level_id, int $team_id, @@ -88,7 +104,10 @@ private static function scorelogFromRow(Map $row): ScoreLog { if (!$mc_result || count($mc_result) === 0 || $refresh) { $db = await self::genDb(); $level_captures = Map {}; - $result = await $db->queryf('SELECT level_id, team_id FROM scores_log'); + $result = + await $db->queryf( + 'SELECT level_id, team_id FROM scores_log LEFT JOIN teams ON scores_log.team_id = teams.id WHERE teams.visible = 1', + ); foreach ($result->mapRows() as $row) { if ($level_captures->contains(intval($row->get('level_id')))) { $level_capture_teams = @@ -166,22 +185,137 @@ private static function scorelogFromRow(Map $row): ScoreLog { } } + // Check if there is a previous score. - ignores team visibility + public static async function genAllPreviousScore( + int $level_id, + int $team_id, + bool $any_team, + bool $refresh = false, + ): Awaitable { + $mc_result = self::getMCRecords('ALL_LEVEL_CAPTURES'); + if (!$mc_result || count($mc_result) === 0 || $refresh) { + $db = await self::genDb(); + $level_captures = Map {}; + $result = await $db->queryf('SELECT level_id, team_id FROM scores_log'); + foreach ($result->mapRows() as $row) { + if ($level_captures->contains(intval($row->get('level_id')))) { + $level_capture_teams = + $level_captures->get(intval($row->get('level_id'))); + invariant( + $level_capture_teams instanceof Vector, + 'level_capture_teams should of type Vector and not null', + ); + $level_capture_teams->add(intval($row->get('team_id'))); + $level_captures->set( + intval($row->get('level_id')), + $level_capture_teams, + ); + } else { + $level_capture_teams = Vector {}; + $level_capture_teams->add(intval($row->get('team_id'))); + $level_captures->add( + Pair {intval($row->get('level_id')), $level_capture_teams}, + ); + } + } + self::setMCRecords('ALL_LEVEL_CAPTURES', new Map($level_captures)); + if ($level_captures->contains($level_id)) { + if ($any_team) { + $level_capture_teams = $level_captures->get($level_id); + invariant( + $level_capture_teams instanceof Vector, + 'level_capture_teams should of type Vector and not null', + ); + $team_id_key = $level_capture_teams->linearSearch($team_id); + if ($team_id_key !== -1) { + $level_capture_teams->removeKey($team_id_key); + } + return intval(count($level_capture_teams)) > 0; + } else { + $level_capture_teams = $level_captures->get($level_id); + invariant( + $level_capture_teams instanceof Vector, + 'level_capture_teams should of type Vector and not null', + ); + $team_id_key = $level_capture_teams->linearSearch($team_id); + return $team_id_key !== -1; + } + } else { + return false; + } + } + invariant( + $mc_result instanceof Map, + 'cache return should of type Map and not null', + ); + if ($mc_result->contains($level_id)) { + if ($any_team) { + $level_capture_teams = $mc_result->get($level_id); + invariant( + $level_capture_teams instanceof Vector, + 'level_capture_teams should of type Vector and not null', + ); + $team_id_key = $level_capture_teams->linearSearch($team_id); + if ($team_id_key !== -1) { + $level_capture_teams->removeKey($team_id_key); + } + return intval(count($level_capture_teams)) > 0; + } else { + $level_capture_teams = $mc_result->get($level_id); + invariant( + $level_capture_teams instanceof Vector, + 'level_capture_teams should of type Vector and not null', + ); + $team_id_key = $level_capture_teams->linearSearch($team_id); + return $team_id_key !== -1; + } + } else { + return false; + } + } + // Get all scores by team. public static async function genAllScoresByTeam( int $team_id, + bool $refresh = false, ): Awaitable> { $db = await self::genDb(); - $result = await $db->queryf( - 'SELECT * FROM scores_log WHERE team_id = %d ORDER BY ts DESC', - $team_id, - ); - - $scores = array(); - foreach ($result->mapRows() as $row) { - $scores[] = self::scorelogFromRow($row); + $mc_result = self::getMCRecords('SCORES_BY_TEAM'); + if (!$mc_result || count($mc_result) === 0) { + $scores = array(); + $result = + await $db->queryf('SELECT * FROM scores_log ORDER BY ts DESC'); + foreach ($result->mapRows() as $row) { + $scores[$row->get('team_id')][] = self::scorelogFromRow($row); + } + self::setMCRecords('SCORES_BY_TEAM', new Map($scores)); + $team_scores = array(); + $scores = new Map($scores); + if ($scores->contains($team_id)) { + $team_scores = $scores->get($team_id); + invariant( + is_array($team_scores), + 'team_scores should an array and not null', + ); + return $team_scores; + } + return $team_scores; + } else { + invariant( + $mc_result instanceof Map, + 'cache return should be a Map of type ScoreLog and not null', + ); + $team_scores = array(); + if ($mc_result->contains($team_id)) { + $team_scores = $mc_result->get($team_id); + invariant( + is_array($team_scores), + 'team_scores should an array and not null', + ); + return $team_scores; + } + return $team_scores; } - - return $scores; } // Get all scores by type. @@ -226,23 +360,37 @@ private static function scorelogFromRow(Map $row): ScoreLog { int $team_id, int $points, string $type, - ): Awaitable { + ): Awaitable { $db = await self::genDb(); - await $db->queryf( - 'INSERT INTO scores_log (ts, level_id, team_id, points, type) VALUES (NOW(), %d, %d, %d, %s)', - $level_id, - $team_id, - $points, - $type, - ); - await ActivityLog::genCaptureLog($team_id, $level_id); - self::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. - ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. - MultiTeam::invalidateMCRecords('ALL_TEAMS'); // Invalidate Memcached MultiTeam data. - MultiTeam::invalidateMCRecords('POINTS_BY_TYPE'); // Invalidate Memcached MultiTeam data. - MultiTeam::invalidateMCRecords('LEADERBOARD'); // Invalidate Memcached MultiTeam data. - MultiTeam::invalidateMCRecords('TEAMS_BY_LEVEL'); // Invalidate Memcached MultiTeam data. - MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. + //'INSERT INTO scores_log (ts, level_id, team_id, points, type) VALUES (NOW(), %d, %d, %d, %s)', + $result = + await $db->queryf( + 'INSERT INTO scores_log (ts, level_id, team_id, points, type) SELECT NOW(), %d, %d, %d, %s FROM DUAL WHERE NOT EXISTS (SELECT * FROM scores_log WHERE level_id = %d AND team_id = %d)', + $level_id, + $team_id, + $points, + $type, + $level_id, + $team_id, + ); + + $captured = $result->numRowsAffected() > 0 ? true : false; + + if ($captured === true) { + await ActivityLog::genCaptureLog($team_id, $level_id); + self::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. + ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. + MultiTeam::invalidateMCRecords('ALL_TEAMS'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('POINTS_BY_TYPE'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('LEADERBOARD'); // Invalidate Memcached MultiTeam data. + $completed_level = await MultiTeam::genCompletedLevel($level_id); + if (count($completed_level) === 0) { + MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. + } + MultiTeam::invalidateMCRecords('TEAMS_BY_LEVEL'); // Invalidate Memcached MultiTeam data. + } + + return $captured; } public static async function genScoreLogUpdate( diff --git a/src/models/Session.php b/src/models/Session.php index 4ca6b62c..7f0b5745 100644 --- a/src/models/Session.php +++ b/src/models/Session.php @@ -173,7 +173,7 @@ private static function sessionFromRow(Map $row): Session { return true; } } - return self::getMCSession($cookie) != false; + return self::getMCSession($cookie) !== false; } public static async function genSessionDataIfExist( @@ -312,9 +312,11 @@ private static function sessionFromRow(Map $row): Session { return; } $db = await self::genDb(); - $expired_sessions = - await self::genExpiredSessionsForCleanup($maxlifetime); - $empty_sessions = await self::genEmptySessionsForCleanup(); + list($expired_sessions, $empty_sessions) = await \HH\Asio\va( + self::genExpiredSessionsForCleanup($maxlifetime), + self::genEmptySessionsForCleanup(), + ); + foreach ($expired_sessions as $session) { $cached_session = self::getMCSession($session->getCookie()); if ($cached_session === false) { @@ -355,17 +357,15 @@ private static function sessionFromRow(Map $row): Session { ); } } - // Clean up expired sessions - await $db->queryf( - 'DELETE FROM sessions WHERE UNIX_TIMESTAMP(last_access_ts) < %d', - time() - $maxlifetime, - ); - // Clean up empty sessions - await $db->queryf( - 'DELETE FROM sessions WHERE IFNULL(data, %s) = %s', - '', - '', - ); + // Clean up expired and empty sessions + $queries = Vector { + sprintf( + 'DELETE FROM sessions WHERE UNIX_TIMESTAMP(last_access_ts) < %d', + time() - $maxlifetime, + ), + 'DELETE FROM sessions WHERE data IS NULL', + }; + await $db->multiQuery($queries); } public static async function genUnprotectedSessions( @@ -447,8 +447,13 @@ private static function sessionFromRow(Map $row): Session { foreach ($all_sessions as $session_key) { $session_key = substr(strstr(substr(strstr($session_key, ':'), 1), ':'), 1); - $cached_sessions[] = $session_key; - $sessions[] = self::getMCSession($session_key); + $session = self::getMCSession($session_key); + if ($session !== false && + $session instanceof Session && + $session->getTeamId() !== 0) { + $cached_sessions[] = $session_key; + $sessions[] = $session; + } } $db = await self::genDb(); $result = await $db->queryf('SELECT * FROM sessions'); @@ -467,12 +472,10 @@ private static function sessionFromRow(Map $row): Session { } private static function setMCSession(string $key, mixed $records): void { - $mc = self::getMc(); $key = str_replace(' ', '', $key); - $mc->set( + self::writeMCCluster( self::$MC_KEY.self::$MC_KEYS->get('SESSIONS').$key, $records, - self::$MC_EXPIRE, ); } @@ -487,18 +490,20 @@ private static function getMCSession(string $key): mixed { public static function invalidateMCSessions(?string $key = null): void { $mc = self::getMc(); $key = str_replace(' ', '', $key); - /* HH_IGNORE_ERROR[4053]: HHVM doesn't beleive there is a getAllKeys() method, there is... */ - $mc_keys = $mc->getAllKeys(); if ($key === null) { + /* HH_IGNORE_ERROR[4053]: HHVM doesn't beleive there is a getAllKeys() method, there is... */ + $mc_keys = $mc->getAllKeys(); $all_sessions = preg_grep( '/'.self::$MC_KEY.self::$MC_KEYS->get('SESSIONS').'/', $mc_keys, ); foreach ($all_sessions as $session_key) { - $mc->delete($session_key); + self::invalidateMCCluster($session_key); } } else { - $mc->delete(self::$MC_KEY.self::$MC_KEYS->get('SESSIONS').$key); + self::invalidateMCCluster( + self::$MC_KEY.self::$MC_KEYS->get('SESSIONS').$key, + ); } } } diff --git a/src/models/Team.php b/src/models/Team.php index 2946a8ae..c9a64fb3 100644 --- a/src/models/Team.php +++ b/src/models/Team.php @@ -36,7 +36,7 @@ public function getVisible(): bool { } public function getName(): string { - return $this->name; + return mb_convert_encoding($this->name, 'UTF-8'); } public function getPasswordHash(): string { @@ -86,7 +86,7 @@ protected static function teamFromRow(Map $row): Team { ): Awaitable { foreach ($elements as $team) { $name = must_have_string($team, 'name'); - $exist = await self::genTeamExist($name); + $exist = await self::genTeamExist($name); // TODO: Combine Awaits if (!$exist) { $team_id = await self::genCreateAll( (bool) must_have_idx($team, 'active'), @@ -97,9 +97,9 @@ protected static function teamFromRow(Map $row): Team { (bool) must_have_idx($team, 'admin'), (bool) must_have_idx($team, 'protected'), (bool) must_have_idx($team, 'visible'), - ); + ); // TODO: Combine Awaits } - await Logo::genSetUsed(must_have_string($team, 'logo'), true); + await Logo::genSetUsed(must_have_string($team, 'logo'), true); // TODO: Combine Awaits } return true; } @@ -179,9 +179,12 @@ public static function regenerateHash(string $password_hash): bool { $ldap = await Configuration::gen('ldap'); if ($ldap->getValue() === '1' && !$team->getAdmin()) { // Get server information from configuration - $ldap_server = await Configuration::gen('ldap_server'); - $ldap_port = await Configuration::gen('ldap_port'); - $ldap_domain_suffix = await Configuration::gen('ldap_domain_suffix'); + list($ldap_server, $ldap_port, $ldap_domain_suffix) = + await \HH\Asio\va( + Configuration::gen('ldap_server'), + Configuration::gen('ldap_port'), + Configuration::gen('ldap_domain_suffix'), + ); $ldapconn = ldap_connect( $ldap_server->getValue(), intval($ldap_port->getValue()), @@ -241,15 +244,16 @@ public static function regenerateHash(string $password_hash): bool { $db = await self::genDb(); // Create team - await $db->queryf( - 'INSERT INTO teams (name, password_hash, logo, created_ts) VALUES (%s, %s, %s, NOW())', - $name, - $password_hash, - $logo, + await \HH\Asio\va( + $db->queryf( + 'INSERT INTO teams (name, password_hash, logo, created_ts) VALUES (%s, %s, %s, NOW())', + $name, + $password_hash, + $logo, + ), + Logo::genSetUsed($logo, true), ); - await Logo::genSetUsed($logo, true); - // Return newly created team_id $result = await $db->queryf( @@ -260,7 +264,12 @@ public static function regenerateHash(string $password_hash): bool { ); Logo::invalidateMCRecords(); - MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. + // Delay rebuilding all cache for the new team, as they won't have any scoring data yet anyway. + MultiTeam::invalidateMCRecords('ALL_TEAMS'); + MultiTeam::invalidateMCRecords('ALL_ACTIVE_TEAMS'); + MultiTeam::invalidateMCRecords('ALL_VISIBLE_TEAMS'); + MultiTeam::invalidateMCRecords('TEAMS_BY_LOGO'); + invariant($result->numRows() === 1, 'Expected exactly one result'); return intval($result->mapRows()[0]['id']); } @@ -279,18 +288,20 @@ public static function regenerateHash(string $password_hash): bool { $db = await self::genDb(); // Create team - await $db->queryf( - 'INSERT INTO teams (name, password_hash, points, logo, active, admin, protected, visible, created_ts) VALUES (%s, %s, %d, %s, %d, %d, %d, %d, NOW())', - $name, - $password_hash, - $points, - $logo, - $active ? 1 : 0, - $admin ? 1 : 0, - $protected ? 1 : 0, - $visible ? 1 : 0, + await \HH\Asio\va( + $db->queryf( + 'INSERT INTO teams (name, password_hash, points, logo, active, admin, protected, visible, created_ts) VALUES (%s, %s, %d, %s, %d, %d, %d, %d, NOW())', + $name, + $password_hash, + $points, + $logo, + $active ? 1 : 0, + $admin ? 1 : 0, + $protected ? 1 : 0, + $visible ? 1 : 0, + ), + Logo::genSetUsed($logo, true), ); - await Logo::genSetUsed($logo, true); // Return newly created team_id $result = @@ -320,7 +331,10 @@ public static function regenerateHash(string $password_hash): bool { $email, $team_id, ); - MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('ALL_TEAMS'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('ALL_ACTIVE_TEAMS'); + MultiTeam::invalidateMCRecords('ALL_VISIBLE_TEAMS'); + MultiTeam::invalidateMCRecords('TEAMS_BY_LOGO'); } // Get a team data. @@ -349,17 +363,18 @@ public static function regenerateHash(string $password_hash): bool { await $db->queryf('SELECT logo FROM teams WHERE id = %d', $team_id); invariant($result->numRows() === 1, 'Expected exactly one result'); $logo_old = strval($result->mapRows()[0]['logo']); - await Logo::genSetUsed($logo_old, false); - await $db->queryf( - 'UPDATE teams SET name = %s, logo = %s , points = %d WHERE id = %d LIMIT 1', - $name, - $logo, - $points, - $team_id, + await \HH\Asio\va( + $db->queryf( + 'UPDATE teams SET name = %s, logo = %s , points = %d WHERE id = %d LIMIT 1', + $name, + $logo, + $points, + $team_id, + ), + Logo::genSetUsed($logo_old, false), + Logo::genSetUsed($logo, true), ); - await Logo::genSetUsed($logo, true); - Logo::invalidateMCRecords(); MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. @@ -376,7 +391,10 @@ public static function regenerateHash(string $password_hash): bool { $password_hash, $team_id, ); - MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('ALL_TEAMS'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('ALL_ACTIVE_TEAMS'); + MultiTeam::invalidateMCRecords('ALL_VISIBLE_TEAMS'); + MultiTeam::invalidateMCRecords('TEAMS_BY_LOGO'); await Session::genDeleteByTeam($team_id); } @@ -387,27 +405,70 @@ public static function regenerateHash(string $password_hash): bool { await $db->queryf('SELECT logo FROM teams WHERE id = %d', $team_id); invariant($result->numRows() === 1, 'Expected exactly one result'); $logo = strval($result->mapRows()[0]['logo']); + await Logo::genSetUsed($logo, false); - await $db->queryf( - 'DELETE FROM teams WHERE id = %d AND protected = 0 LIMIT 1', - $team_id, - ); - await $db->queryf( - 'DELETE FROM registration_tokens WHERE team_id = %d', - $team_id, - ); - await $db->queryf('DELETE FROM scores_log WHERE team_id = %d', $team_id); - await $db->queryf('DELETE FROM hints_log WHERE team_id = %d', $team_id); - await $db->queryf( - 'DELETE FROM failures_log WHERE team_id = %d', - $team_id, - ); + $queries = Vector { + sprintf( + 'DELETE FROM teams WHERE id = %d AND protected = 0 LIMIT 1', + $team_id, + ), + sprintf( + 'DELETE FROM registration_tokens WHERE team_id = %d', + $team_id, + ), + sprintf('DELETE FROM teams_oauth WHERE team_id = %d', $team_id), + sprintf('DELETE FROM scores_log WHERE team_id = %d', $team_id), + sprintf('DELETE FROM hints_log WHERE team_id = %d', $team_id), + sprintf('DELETE FROM failures_log WHERE team_id = %d', $team_id), + sprintf( + 'DELETE FROM activity_log WHERE subject = "Team:%d"', + $team_id, + ), + }; + await $db->multiQuery($queries); MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. + ScoreLog::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. + HintLog::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. await Session::genDeleteByTeam($team_id); } + public static async function genSetTeamName( + int $team_id, + string $team_name, + ): Awaitable { + $db = await self::genDb(); + + $team_name = trim($team_name); + + if ($team_name === '') { + return false; + } + + $shortname = substr($team_name, 0, 20); + + $team_exists = await Team::genTeamExist($shortname); + if ($team_exists === true) { + return false; + } else { + $team = await self::genTeam($team_id); + await self::genUpdate( + $shortname, + $team->getLogo(), + $team->getPoints(), + $team_id, + ); + MultiTeam::invalidateMCRecords('ALL_TEAMS'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('ALL_ACTIVE_TEAMS'); + MultiTeam::invalidateMCRecords('ALL_VISIBLE_TEAMS'); + MultiTeam::invalidateMCRecords('TEAMS_BY_LOGO'); + ScoreLog::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. + ActivityLog::invalidateMCRecords(); // Invalidate Memcached ActivityLog data. + return true; + } + } + // Enable or disable teams by passing 1 or 0. public static async function genSetStatus( int $team_id, @@ -734,29 +795,149 @@ public static function regenerateHash(string $password_hash): bool { Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control data. } - public static async function genGetLiveSyncKey( - int $team_id, + public static async function genAuthTokenExists( string $type, - ): Awaitable { + string $token, + ): Awaitable { $db = await self::genDb(); - $result = await $db->queryf( - 'SELECT * FROM livesync WHERE team_id = %d AND type = %s', + + $team_id_result = await $db->queryf( + 'SELECT team_id FROM teams_oauth WHERE type = %s AND token = %s', + $type, + $token, + ); + + if ($team_id_result->numRows() === 1) { + return true; + } else { + return false; + } + } + + public static async function genTeamOAuthTokenExists( + string $type, + int $team_id, + ): Awaitable { + $db = await self::genDb(); + + $team_id_result = await $db->queryf( + 'SELECT id FROM teams_oauth WHERE type = %s AND team_id = %d', + $type, $team_id, + ); + + if ($team_id_result->numRows() === 1) { + return true; + } else { + return false; + } + } + + public static async function genTeamFromOAuthToken( + string $type, + string $token, + ): Awaitable { + $db = await self::genDb(); + + $team_id_result = await $db->queryf( + 'SELECT team_id FROM teams_oauth WHERE type = %s AND token = %s', $type, + $token, ); - invariant($result->numRows() === 1, 'Expected exactly one result'); - $username = strval(must_have_idx($result->mapRows()[0], 'username')); - $key_from_db = strval(must_have_idx($result->mapRows()[0], 'sync_key')); + $team_id = + intval(must_have_idx($team_id_result->mapRows()[0], 'team_id')); + $team = await self::genTeam($team_id); + return $team; + } - switch ($type) { - case 'fbctf': - $key = self::generateHash($key_from_db); - break; - // FALLTHROUGH - default: - $key = $key_from_db; - break; + public static async function genSetOAuthToken( + int $team_id, + string $type, + string $token, + ): Awaitable { + $db = await self::genDb(); + + $queries = Vector { + sprintf( + 'SELECT id FROM teams_oauth WHERE type = "%s" AND token = "%s"', + $db->escapeString($type), + $db->escapeString($token), + ), + sprintf( + 'SELECT id FROM teams_oauth WHERE team_id = %d AND type = "%s"', + $team_id, + $db->escapeString($type), + ), + }; + list($oauth_exists_result, $current_id_result) = + await $db->multiQuery($queries); + + if ($oauth_exists_result->numRows() > 0) { + return false; + } + + if ($current_id_result->numRows() === 1) { + $result = await $db->queryf( + 'UPDATE teams_oauth SET token = %s WHERE id = %d', + $token, + intval(must_have_idx($current_id_result->mapRows()[0], 'id')), + ); + if ($result) { + return true; + } + } else { + $result = await $db->queryf( + 'INSERT INTO teams_oauth (type, team_id, token) VALUES (%s, %d, %s)', + $type, + $team_id, + $token, + ); + if ($result) { + return true; + } + } + return false; + + } + + public static async function genGetLiveSyncKey( + int $team_id, + string $type, + ): Awaitable { + $db = await self::genDb(); + if ($type === 'general') { + $team = await self::genTeam($team_id); + $username = $team->getName(); + $key = ''; + } else { + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE team_id = %d AND type = %s', + $team_id, + $type, + ); + invariant($result->numRows() === 1, 'Expected exactly one result'); + + $username = strval(must_have_idx($result->mapRows()[0], 'username')); + $key_from_db = strval(must_have_idx($result->mapRows()[0], 'sync_key')); + + switch ($type) { + case 'fbctf': + $key = self::generateHash($key_from_db); + break; + case 'facebook_oauth': + $key = $key_from_db; + $username = ''; + break; + case 'google_oauth': + $key = $key_from_db; + $username = ''; + break; + // FALLTHROUGH + default: + $key = $key_from_db; + break; + } } return strval($type.":".$username.":".$key); @@ -864,6 +1045,9 @@ public static function regenerateHash(string $password_hash): bool { $type, ); break; + case 'facebook_oauth': + return await Integration::genFacebookThirdPartyExists($key); + break; case 'google_oauth': $result = await $db->queryf( 'SELECT * FROM livesync WHERE sync_key = %s AND type = %s', @@ -909,6 +1093,7 @@ public static function regenerateHash(string $password_hash): bool { string $key, ): Awaitable { $db = await self::genDb(); + $email = ''; invariant(strpos($key, ':'), "Invalid live sync key"); list($type, $username, $key) = explode(':', $key); @@ -922,6 +1107,18 @@ public static function regenerateHash(string $password_hash): bool { ); invariant($result->numRows() > 0, 'Expected at least one result'); break; + case 'facebook_oauth': + $email = await Integration::genFacebookThirdPartyEmail($key); + invariant( + $email !== '', + 'Expected an email from genFacebookThirdPartyEmail, non returned.', + ); + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE username = %s AND type = %s', + $email, + $type, + ); + break; case 'google_oauth': $result = await $db->queryf( 'SELECT * FROM livesync WHERE sync_key = %s AND type = %s', @@ -953,6 +1150,13 @@ public static function regenerateHash(string $password_hash): bool { return $team; } break; + case 'facebook_oauth': + if (strval($email) === strval($username)) { + $team_id = intval(must_have_idx($row, 'team_id')); + $team = await self::genTeam($team_id); + return $team; + } + break; // FALLTHROUGH default: if (strval($key) === strval($key_from_db)) { diff --git a/src/models/Token.php b/src/models/Token.php index f4e65d5f..a6817aeb 100644 --- a/src/models/Token.php +++ b/src/models/Token.php @@ -43,10 +43,7 @@ private static function tokenFromRow(Map $row): Token { private static function generate(): string { $token_len = 15; - $crypto_strong = True; - return md5( - base64_encode(openssl_random_pseudo_bytes($token_len, $crypto_strong)), - ); + return md5(base64_encode(random_bytes($token_len))); } // Create token. diff --git a/src/scripts/autorun.php b/src/scripts/autorun.php index 9f45bffc..19c07cf0 100644 --- a/src/scripts/autorun.php +++ b/src/scripts/autorun.php @@ -8,6 +8,7 @@ require_once (__DIR__.'/../Db.php'); require_once (__DIR__.'/../Utils.php'); require_once (__DIR__.'/../models/Model.php'); +require_once (__DIR__.'/../models/Cache.php'); require_once (__DIR__.'/../models/Importable.php'); require_once (__DIR__.'/../models/Exportable.php'); require_once (__DIR__.'/../models/Level.php'); @@ -19,6 +20,8 @@ require_once (__DIR__.'/../models/ScoreLog.php'); require_once (__DIR__.'/../models/HintLog.php'); require_once (__DIR__.'/../models/FailureLog.php'); +require_once (__DIR__.'/../models/Announcement.php'); +require_once (__DIR__.'/../models/ActivityLog.php'); while (1) { \HH\Asio\join(Control::genAutoRun()); diff --git a/src/scripts/bases.php b/src/scripts/bases.php index 8e4fb0f3..0e0746e2 100644 --- a/src/scripts/bases.php +++ b/src/scripts/bases.php @@ -8,6 +8,7 @@ require_once (__DIR__.'/../Db.php'); require_once (__DIR__.'/../Utils.php'); require_once (__DIR__.'/../models/Model.php'); +require_once (__DIR__.'/../models/Cache.php'); require_once (__DIR__.'/../models/Importable.php'); require_once (__DIR__.'/../models/Exportable.php'); require_once (__DIR__.'/../models/Level.php'); @@ -17,6 +18,8 @@ require_once (__DIR__.'/../models/ScoreLog.php'); require_once (__DIR__.'/../models/Control.php'); require_once (__DIR__.'/../models/MultiTeam.php'); +require_once (__DIR__.'/../models/Announcement.php'); +require_once (__DIR__.'/../models/ActivityLog.php'); $conf_game = \HH\Asio\join(Configuration::gen('game')); while ($conf_game->getValue() === '1') { @@ -35,14 +38,14 @@ if ($response['response']) { $code = 0; $json_r = json_decode($response['response'])[0]; - $teamname = $json_r->team; + $team_name = $json_r->team; // Give points to the team if exists - if (\HH\Asio\join(Team::genTeamExist($teamname))) { - $team = \HH\Asio\join(Team::genTeamByName($teamname)); + if (\HH\Asio\join(Team::genTeamExist($team_name))) { + $team = \HH\Asio\join(Team::genTeamByName($team_name)); \HH\Asio\join(Level::genScoreBase($response['id'], $team->getId())); //echo "Points\n"; } - //echo "Base(".strval($response['id']).") taken by ".$teamname."\n"; + //echo "Base(".strval($response['id']).") taken by ".$team_name."\n"; } else { $code = -1; //echo "Base(".strval($response['id']).") is DOWN\n"; @@ -58,4 +61,10 @@ // Wait until next iteration $bases_cycle = \HH\Asio\join(Configuration::gen('bases_cycle')); sleep(intval($bases_cycle->getValue())); + + // Flush the local cache before the next cycle to ensure the game is still running and the configuration of the bases hasn't changed (the script runs continuously). + Model::deleteLocalCache(); + + // Get current game status + $conf_game = \HH\Asio\join(Configuration::gen('game')); } diff --git a/src/scripts/liveimport.php b/src/scripts/liveimport.php index c5ea2c50..9b138d05 100644 --- a/src/scripts/liveimport.php +++ b/src/scripts/liveimport.php @@ -8,6 +8,7 @@ require_once (__DIR__.'/../Db.php'); require_once (__DIR__.'/../Utils.php'); require_once (__DIR__.'/../models/Model.php'); +require_once (__DIR__.'/../models/Cache.php'); require_once (__DIR__.'/../models/Importable.php'); require_once (__DIR__.'/../models/Exportable.php'); require_once (__DIR__.'/../models/Level.php'); @@ -19,7 +20,11 @@ require_once (__DIR__.'/../models/Country.php'); require_once (__DIR__.'/../models/Control.php'); require_once (__DIR__.'/../models/MultiTeam.php'); +require_once (__DIR__.'/../models/Integration.php'); require_once (__DIR__.'/../models/Model.php'); +require_once + (__DIR__.'/../../vendor/facebook/graph-sdk/src/Facebook/autoload.php') +; $long_opts = array('url:', 'sleep:', 'disable-ssl-verification', 'debug'); $options = getopt('', $long_opts); @@ -65,22 +70,22 @@ class LiveSyncImport { continue; } foreach ($data as $level) { - $mandatories_set = await self::genMandatoriesSet($level); + $mandatories_set = await self::genMandatoriesSet($level); // TODO: Combine Awaits if ($mandatories_set === false) { self::debug(true, $url, '!!!', 'Mandatory Values Not Set'); continue; } - $level = await self::genDefaults($level); - $level_id = await self::genLevel($url, $level, $debug); + $level = await self::genDefaults($level); // TODO: Combine Awaits + $level_id = await self::genLevel($url, $level, $debug); // TODO: Combine Awaits $teams = - await self::genTeamCaptures($url, $level, $level_id, $debug); + await self::genTeamCaptures($url, $level, $level_id, $debug); // TODO: Combine Awaits await self::genRecalculateScores( $url, $level, $level_id, $teams, $debug, - ); + ); // TODO: Combine Awaits } } else { self::debug( @@ -183,8 +188,10 @@ class LiveSyncImport { } $level_exists = await self::genLevelExists($level); if ($level_exists === false) { - $category_id = await self::genCategory($url, $level, $debug); - $country = await Country::genCountry(strval($level->entity_iso_code)); + list($category_id, $country) = await \HH\Asio\va( + self::genCategory($url, $level, $debug), + Country::genCountry(strval($level->entity_iso_code)), + ); $country_id = $country->getId(); $level_id = await Level::genCreate( strval($level->type), @@ -362,6 +369,10 @@ function($a, $b) { $teams_array = array(); $teams = await self::genTeamDefaults($teams); foreach ($teams as $team_livesync_key => $team_data) { + list($type, $username, $key) = explode(':', $team_livesync_key); + if ($type === 'general') { + continue; + } $team_exists = await Team::genLiveSyncKeyExists(strval($team_livesync_key)); if ($team_exists === true) { @@ -431,7 +442,7 @@ function($a, $b) { int $hint, bool $debug, ): Awaitable { - $team = await Team::genTeam($team_id); + $team = await MultiTeam::genTeam($team_id); $team_name = $team->getName(); $hint_used = false; if ($hint === 1) { @@ -475,9 +486,12 @@ function($a, $b) { bool $debug, ): Awaitable { if ($capture === 1) { - $team = await Team::genTeam($team_id); + list($team, $level_capture) = await \HH\Asio\va( + MultiTeam::genTeam($team_id), + Level::genScoreLevel($level_id, $team_id), + ); $team_name = $team->getName(); - $level_capture = await Level::genScoreLevel($level_id, $team_id); + if ($level_capture === true) { $scorelog = await ScoreLog::genLevelScoreByTeam($team_id, $level_id); await ScoreLog::genScoreLogUpdate( @@ -518,7 +532,7 @@ function($a, $b) { bool $debug, ): Awaitable { if (($hint === 1) && ($hint_used === true)) { - $team = await Team::genTeam($team_id); + $team = await MultiTeam::genTeam($team_id); $team_name = $team->getName(); await Team::genUpdate( strval($team_name), @@ -542,7 +556,7 @@ function($a, $b) { if (intval($team_data['capture']) === 0) { continue; } - $team = await Team::genTeam($team_id); + $team = await MultiTeam::genTeam($team_id); $team_name = $team->getName(); $current_bonus = $level->bonus - (intval($level->bonus_dec) * $level_captured); @@ -554,8 +568,10 @@ function($a, $b) { $existing_points = $scorelog->getPoints(); $total_points = $team->getPoints(); $total_points += $points - $existing_points; - await Team::genTeamUpdatePoints($team_id, $total_points); - await ScoreLog::genUpdateScoreLogBonus($level_id, $team_id, $points); + await \HH\Asio\va( + Team::genTeamUpdatePoints($team_id, $total_points), + ScoreLog::genUpdateScoreLogBonus($level_id, $team_id, $points), + ); $level_captured++; } if ($level_captured > 0) { @@ -592,4 +608,7 @@ public static function debug( LiveSyncImport::genProcess($urls, $check_certificates, $debug), ); sleep($sleep); + + // Flush the local cache before the next import cycle to ensure we get up-to-date team and level data (the script runs continuously). + Model::deleteLocalCache(); } diff --git a/src/scripts/progressive.php b/src/scripts/progressive.php index fde2fdef..0d9e75c1 100644 --- a/src/scripts/progressive.php +++ b/src/scripts/progressive.php @@ -7,6 +7,7 @@ require_once (__DIR__.'/../Db.php'); require_once (__DIR__.'/../Utils.php'); require_once (__DIR__.'/../models/Model.php'); +require_once (__DIR__.'/../models/Cache.php'); require_once (__DIR__.'/../models/Configuration.php'); require_once (__DIR__.'/../models/Progressive.php'); diff --git a/src/static/css/scss/_admin.scss b/src/static/css/scss/_admin.scss index e887c034..c69109b5 100644 --- a/src/static/css/scss/_admin.scss +++ b/src/static/css/scss/_admin.scss @@ -60,11 +60,20 @@ Admin Nav } } +.admin-nav-controls { + text-align: center; + padding-top: 20px; + + a { + margin: 20px auto 20px; + border-top: 1px solid $blue-border; + } +} + .admin-nav--footer { @include purista-bold; text-align: center; - padding-top: 20px; .branding-el { display: block; diff --git a/src/static/css/scss/_modals.scss b/src/static/css/scss/_modals.scss index 8ea7aea8..a5e69092 100644 --- a/src/static/css/scss/_modals.scss +++ b/src/static/css/scss/_modals.scss @@ -53,6 +53,11 @@ .fb-form { padding: 40px; } + + .fb-form-no-padding { + padding: 0px !important; + } + } .modal-title { diff --git a/src/static/js/admin.js b/src/static/js/admin.js index 69973c52..46c32abb 100644 --- a/src/static/js/admin.js +++ b/src/static/js/admin.js @@ -574,6 +574,14 @@ function flushMemcached() { sendAdminRequest(flush_memcached, true); } +//Reset Game Schedule +function resetGameSchedule() { + var reset_game_schedule = { + action: 'reset_game_schedule' + }; + sendAdminRequest(reset_game_schedule, true); +} + // Create tokens function createTokens() { var create_data = { @@ -1099,6 +1107,8 @@ module.exports = { exportCurrentCategories(); } else if (action === 'flush-memcached') { flushMemcached(); + } else if (action === 'reset-game-schedule') { + resetGameSchedule(); } else if (action === 'create-tokens') { createTokens(); } else if (action === 'export-tokens') { @@ -1212,7 +1222,7 @@ module.exports = { var start_day = $('input[type="number"][name="fb--schedule--start_day"]')[0].value; var start_hour = $('input[type="number"][name="fb--schedule--start_hour"]')[0].value; var start_min = $('input[type="number"][name="fb--schedule--start_min"]')[0].value; - var start_ts = new Date(start_month + "/" + start_day + "/" + start_year + " " + start_hour + ":" + start_min).getTime() / 1000; + var start_ts = Date.UTC(start_year, start_month - 1, start_day, start_hour, start_min) / 1000; if ($.isNumeric(start_ts)) { changeConfiguration("start_ts", start_ts); changeConfiguration("next_game", start_ts); @@ -1222,7 +1232,7 @@ module.exports = { var end_day = $('input[type="number"][name="fb--schedule--end_day"]')[0].value; var end_hour = $('input[type="number"][name="fb--schedule--end_hour"]')[0].value; var end_min = $('input[type="number"][name="fb--schedule--end_min"]')[0].value; - var end_ts = new Date(end_month + "/" + end_day + "/" + end_year + " " + end_hour + ":" + end_min).getTime() / 1000; + var end_ts = Date.UTC(end_year, end_month - 1, end_day, end_hour, end_min) / 1000; if ($.isNumeric(end_ts)) { changeConfiguration("end_ts", end_ts); } diff --git a/src/static/js/app.js b/src/static/js/app.js index 62a912e1..54beffe0 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -50,8 +50,15 @@ function enableAdminActiveState() { } $(document).ready(function() { - if (window.innerWidth < 960) { - window.location = '/index.php?page=mobile'; + var page_location = window.location.pathname + window.location.search; + if (window.innerWidth < 960 && page_location != '/index.php?page=mobile') { + window.location = '/index.php?page=mobile'; + } else if (window.innerWidth < 960 && page_location == '/index.php?page=mobile') { + setTimeout(function() { + window.location = '/index.php'; + }, 2000); + } else if (window.innerWidth >= 960 && page_location === '/index.php?page=mobile') { + window.location = '/index.php'; } FB_CTF.init(); diff --git a/src/static/js/fb-ctf.js b/src/static/js/fb-ctf.js index e1b22598..a59058cc 100644 --- a/src/static/js/fb-ctf.js +++ b/src/static/js/fb-ctf.js @@ -275,8 +275,18 @@ function setupInputListeners() { $mapSvg, $map, $countryHover, - reload = true, - reload_team = true; + refresh_active_config = false, + refresh_active_country = false, + refresh_active_map = false, + refresh_active_captures = false, + refresh_active_announcment = false, + refresh_active_activity = false, + refresh_active_team_data = false, + refresh_active_team_module = false, + refresh_active_leaderboard = false, + refresh_active_clear_map = false, + refresh_active_filter = false, + refresh_active_session = false; /** @@ -511,89 +521,67 @@ function setupInputListeners() { // Load initial activity loadActivityModule(); + + //Get current team captures + getCaptureData(); - // Configuration reloader + // Configuration/Session reloader setInterval(function() { loadConfData(); + checkActiveSession(); }, FB_CTF.data.CONF.refreshConf); // Countries and other modules - var count = 0; setInterval(function() { - if (reload == true || count > 1){ - reload = false; - if (FB_CTF.data.CONF.gameboard === '1') { - // Map - getCountryData(); - refreshMapData(); - // Announcements - if (Widget.getWidgetStatus('Announcements') === 'open') { - loadAnnouncementsModule(); - } - // Filter - if (Widget.getWidgetStatus('Filter') === 'open') { - loadSavedFilterModule(); - } - // Activity - if (Widget.getWidgetStatus('Activity') === 'open') { - loadActivityModule(); - } - } else { - clearMapData(); - clearAnnouncements(); - clearActivity(); + if (FB_CTF.data.CONF.gameboard === '1') { + // Map + getCountryData(); + refreshMapData(); + getCaptureData(); + // Announcements + if (Widget.getWidgetStatus('Announcements') === 'open') { + loadAnnouncementsModule(); } - } - - if (reload == false){ - count += 1; - } - - // reset counter - if (count > 1){ - count = 0; - reload = true; + // Filter + if (Widget.getWidgetStatus('Filter') === 'open') { + loadSavedFilterModule(); + } + // Activity + if (Widget.getWidgetStatus('Activity') === 'open') { + loadActivityModule(); + } + } else { + clearMapData(); + clearAnnouncements(); + clearActivity(); } }, FB_CTF.data.CONF.refreshMap); // Teams - var teams_count = 0; setInterval(function() { - if (reload_team == true || teams_count > 1){ - reload_team = false; - if (FB_CTF.data.CONF.gameboard === '1') { - // Teams - loadTeamData(); - if (Widget.getWidgetStatus('Teams') === 'open') { - loadTeamsModule(); - } - if (Widget.getWidgetStatus('Leaderboard') === 'open') { - loadLeaderboardModule(); - } - } else { - clearTeams(); - clearLeaderboard(); + if (FB_CTF.data.CONF.gameboard === '1') { + // Teams + loadTeamData(); + if (Widget.getWidgetStatus('Teams') === 'open') { + loadTeamsModule(); } - } - - if (reload_team == false){ - teams_count += 1; - } - - // reset team counter - if (teams_count > 1){ - teams_count = 0; - reload_team = true; + if (Widget.getWidgetStatus('Leaderboard') === 'open') { + loadLeaderboardModule(); + } + } else { + clearTeams(); + clearLeaderboard(); } }, FB_CTF.data.CONF.refreshMap); // Forcefully refreshing all modules every minute setInterval(function() { - loadAnnouncementsModule(); - loadSavedFilterModule(); - loadActivityModule(); - loadTeamsModule(); - loadLeaderboardModule(); + checkActiveSession(true); + loadAnnouncementsModule(true); + loadSavedFilterModule(true); + loadActivityModule(true); + loadTeamsModule(true); + loadLeaderboardModule(true); loadClockModule(); }, 60000); @@ -927,7 +915,7 @@ function setupInputListeners() { owner = data ? data.owner : '', attachments = data ? data.attachments : '', links = data ? data.links : ''; - + $('.country-name', $container).text(country); $('.country-title', $container).text(title); $('input[name=level_id]', $container).attr('value', level_id); @@ -975,6 +963,12 @@ function setupInputListeners() { $('.answer_no_bases').addClass('completely-hidden'); } + // Hide flag submission for captured levels + if ($.inArray(level_id, FB_CTF.data.CAPTURES) != -1) { + $('.answer_no_bases').addClass('completely-hidden'); + $('.answer_captured').removeClass('completely-hidden'); + } + // // event listeners // @@ -1047,9 +1041,19 @@ function setupInputListeners() { var responseData = JSON.parse(data); if (responseData.result === 'OK') { console.log('OK'); - $('input[name=answer]', $container).css("background-color", "#1f7a1f"); + $($container).on('keypress', function(e) { + if (e.keyCode == 13) { + e.preventDefault(); + } + }); $('.js-trigger-score', $container).text('YES!'); + $('input[name=answer]', $container).css("background-color", "#1f7a1f"); + $('.answer_no_bases > .fb-cta.cta--yellow.js-trigger-score').removeClass('js-trigger-score'); + refreshMapData(); // Refresh map so capture shows up right away + getCaptureData(); // Refresh captured levels so we can't reload the modal and see a submit button setTimeout(function() { + $('.answer_no_bases').addClass('completely-hidden'); + $('.answer_captured').removeClass('completely-hidden'); $('.js-close-modal', $container).click(); }, 2000); } else { @@ -1328,10 +1332,16 @@ function setupInputListeners() { var get = $.get(modulePath, function(data) { $self.html(data); - }).error(function() { + }).error(function(jqxhr, status, error) { console.error("There was a problem retrieving the module."); console.log(modulePath); + console.log(status); + console.log(error); console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } }); deferredArray.push(get); @@ -1361,9 +1371,15 @@ function setupInputListeners() { $mapSvg = $('#fb-gameboard-map'); $countryHover = $('[class~="country-hover"]', $mapSvg); enableClickAndDrag.init(); - }, 'html').error(function() { + }, 'html').error(function(jqxhr, status, error) { console.error("There was a problem loading the svg map"); + console.log(status); + console.log(error); console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } }); } @@ -1381,9 +1397,15 @@ function setupInputListeners() { $mapSvg = $('#fb-gameboard-map'); $countryHover = $('[class~="country-hover"]', $mapSvg); enableClickAndDrag.init(); - }, 'html').error(function() { + }, 'html').error(function(jqxhr, status, error) { console.error("There was a problem loading the svg map"); + console.log(status); + console.log(error); console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } }); } @@ -1397,9 +1419,15 @@ function setupInputListeners() { $listview = $('.fb-listview'); $listview.html(data); listviewEventListeners($listview); - }, 'html').error(function() { + }, 'html').error(function(jqxhr, status, error) { console.error("There was a problem loading the List View"); + console.log(status); + console.log(error); console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } }); } @@ -1415,31 +1443,48 @@ function setupInputListeners() { success_callback(); } }) - .error(function() { + .error(function(jqxhr, status, error) { console.error("There was a problem retrieving the module."); console.log(loadPath); + console.log(status); + console.log(error); console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } }); } /** * load the teams module */ - function loadTeamsModule() { - var teamsModulePath = 'inc/gameboard/modules/teams.php'; - var teamsTargetSelector = 'aside[data-module="teams"]'; - - return loadModuleGeneric(teamsModulePath, teamsTargetSelector); + function loadTeamsModule(force = false) { + if (refresh_active_team_module === false || force === true) { + refresh_active_team_module = true; + var teamsModulePath = 'inc/gameboard/modules/teams.php'; + var teamsTargetSelector = 'aside[data-module="teams"]'; + + return loadModuleGeneric(teamsModulePath, teamsTargetSelector, function() { + refresh_active_team_module = false; + }); + } } /** * load the leaderboard module */ - function loadLeaderboardModule() { - var leaderboardModulePath = 'inc/gameboard/modules/leaderboard.php'; - var leaderboardSelector = 'aside[data-module="leaderboard"]'; + function loadLeaderboardModule(force = false) { + if (refresh_active_leaderboard === false || force === true) { + refresh_active_leaderboard = true; + + var leaderboardModulePath = 'inc/gameboard/modules/leaderboard.php'; + var leaderboardSelector = 'aside[data-module="leaderboard"]'; - return loadModuleGeneric(leaderboardModulePath, leaderboardSelector); + return loadModuleGeneric(leaderboardModulePath, leaderboardSelector, function() { + refresh_active_leaderboard = false; + }); + } } /** @@ -1459,163 +1504,269 @@ function setupInputListeners() { /** * load the team data */ - function loadTeamData() { - var loadPath = 'data/teams.php'; - - return $.get(loadPath, function(data) { - FB_CTF.data.TEAMS = data; - var df = $.Deferred(); - reload_team = true; - return df.resolve(FB_CTF.data.TEAMS); - }, 'json').error(function(jqhxr, status, error) { - console.error("There was a problem retrieving the team data."); - console.log(loadPath); - console.log(status); - console.log(error); - console.error("/error"); - console.error("Team data request failed"); - reload_team = false; - }); + function loadTeamData(force = false) { + if (refresh_active_team_data === false || force === true) { + refresh_active_team_data = true; + var loadPath = 'data/teams.php'; + + return $.get(loadPath, function(data) { + FB_CTF.data.TEAMS = data; + var df = $.Deferred(); + return df.resolve(FB_CTF.data.TEAMS); + }, 'json').error(function(jqxhr, status, error) { + console.error("There was a problem retrieving the team data."); + console.log(loadPath); + console.log(status); + console.log(error); + console.error("/error"); + console.error("Team data request failed"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } + }).done(function() { + refresh_active_team_data = false; + }); + } + } + + /** + * load the team data + */ + function getCaptureData(force = false) { + if (refresh_active_captures === false || force === true) { + refresh_active_captures = true; + var loadPath = 'data/captures.php'; + + return $.get(loadPath, function(data) { + FB_CTF.data.CAPTURES = data; + var df = $.Deferred(); + return df.resolve(FB_CTF.data.CAPTURES); + }, 'json').error(function(jqxhr, status, error) { + console.error("There was a problem retrieving the captures data."); + console.log(loadPath); + console.log(status); + console.log(error); + console.error("/error"); + console.error("Captures data request failed"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } + }).done(function() { + refresh_active_captures = false; + }); + } } /** * load the announcements module */ - function loadAnnouncementsModule() { - var announcementsModulePath = 'inc/gameboard/modules/announcements.php'; - var announcementsTargetSelector = 'aside[data-module="announcements"]'; - - return loadModuleGeneric(announcementsModulePath, announcementsTargetSelector); + function loadAnnouncementsModule(force = false) { + if (refresh_active_announcment === false || force === true) { + refresh_active_announcment = true; + var announcementsModulePath = 'inc/gameboard/modules/announcements.php'; + var announcementsTargetSelector = 'aside[data-module="announcements"]'; + + return loadModuleGeneric(announcementsModulePath, announcementsTargetSelector, function() { + refresh_active_announcment = false; + }); + } } /** * load the filter module */ - function loadFilterModule() { - var filterModulePath = 'inc/gameboard/modules/filter.php'; - var filterTargetSelector = 'aside[data-module="filter"]'; - - return loadModuleGeneric( - filterModulePath, - filterTargetSelector, - function() { - Filter.rememberFilters(filterList); - } - ); + function loadFilterModule(force = false) { + if (refresh_active_filter === false || force === true) { + refresh_active_filter = true; + var filterModulePath = 'inc/gameboard/modules/filter.php'; + var filterTargetSelector = 'aside[data-module="filter"]'; + + return loadModuleGeneric( + filterModulePath, + filterTargetSelector, + function() { + refresh_active_filter = false; + Filter.rememberFilters(filterList); + } + ); + } } /** * wrapper to load and save/remember the filter module */ - function loadSavedFilterModule() { + function loadSavedFilterModule(force = false) { // Update variable for all filters to remember them filterList = Filter.detectFilters(); // Load filter module - return loadFilterModule(); + return loadFilterModule(force); } /** * load the activity module */ - function loadActivityModule() { - var activityModulePath = 'inc/gameboard/modules/activity.php'; - var activityTargetSelector = 'aside[data-module="activity"]'; - - return loadModuleGeneric(activityModulePath, activityTargetSelector); + function loadActivityModule(force = false) { + if (refresh_active_activity === false || force === true) { + refresh_active_activity = true; + var activityModulePath = 'inc/gameboard/modules/activity.php'; + var activityTargetSelector = 'aside[data-module="activity"]'; + + return loadModuleGeneric(activityModulePath, activityTargetSelector, function() { + refresh_active_activity = false; + }); + } } /** * load the configuration data */ - function loadConfData() { - var loadPath = 'data/configuration.php'; - - return $.get(loadPath, function(data) { - FB_CTF.data.CONF = data; - var df = $.Deferred(); - reload = true; - return df.resolve(FB_CTF.data.CONF); - }, 'json').error(function(jqhxr, status, error) { - console.error("There was a problem retrieving the conf data."); - console.log(loadPath); - console.log(status); - console.log(error); - console.error("/error"); - reload = false; - }); + function loadConfData(force = false) { + if (refresh_active_config === false || force === true) { + refresh_active_config = true; + var loadPath = 'data/configuration.php'; + + return $.get(loadPath, function(data) { + FB_CTF.data.CONF = data; + var df = $.Deferred(); + return df.resolve(FB_CTF.data.CONF); + }, 'json').error(function(jqxhr, status, error) { + console.error("There was a problem retrieving the conf data."); + console.log(loadPath); + console.log(status); + console.log(error); + console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } + }).done(function() { + refresh_active_config = false; + }); + } + } + + /** + * verify and active session, or redirect to login + */ + function checkActiveSession(force = false) { + if (refresh_active_session === false || force === true) { + refresh_active_session = true; + var loadPath = 'data/session.php'; + + return $.get(loadPath, function(data, response, xhr) { + if (xhr.getResponseHeader('Login-Page') === "true") { + console.log('Session is not active'); + console.log("Redirecting to '/index.php?page=login'"); + window.location.replace('/index.php?page=login'); + } + }).error(function(jqxhr, status, error) { + console.error("There was a problem retrieving the session data."); + console.log(loadPath); + console.log(status); + console.log(error); + console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } + }).done(function() { + refresh_active_session = false; + }); + } } /** * refresh the map data */ - function refreshMapData() { - var loadPath = 'data/map-data.php'; - - return $.get(loadPath, function(data) { - $.each(data, function(key, value) { - // First we clear all - $('#' + key)[0].classList.remove('active'); - $('#' + key)[0].parentNode.removeAttribute('data-captured'); - $('#' + key)[0].parentNode.children[1].classList.remove("captured--you"); - $('#' + key)[0].parentNode.children[1].classList.remove("captured--opponent"); - - // Active country - if (value.status === 'active') { - if (!$('#' + key).hasClass('active')) { - $('#' + key)[0].classList.add('active'); + function refreshMapData(force = false) { + if (refresh_active_map === false || force === true) { + refresh_active_map = true; + + var loadPath = 'data/map-data.php'; + + return $.get(loadPath, function(data) { + FB_CTF.data.MAP = data; + + $.each(data, function(key, value) { + + // First we clear all + $('#' + key)[0].classList.remove('active'); + $('#' + key)[0].parentNode.removeAttribute('data-captured'); + $('#' + key)[0].parentNode.children[1].classList.remove("captured--you"); + $('#' + key)[0].parentNode.children[1].classList.remove("captured--opponent"); + + // Active country + if (value.status === 'active') { + if (!$('#' + key).hasClass('active')) { + $('#' + key)[0].classList.add('active'); + } } + /*else { // Inactive country + $('#' + key)[0].classList.remove('active'); + $('#' + key)[0].parentNode.removeAttribute('data-captured'); + $('#' + key)[0].parentNode.children[1].classList.remove("captured--you"); + $('#' + key)[0].parentNode.children[1].classList.remove("captured--opponent"); + }*/ + if (value.captured == 'you') { + //$('#' + key)[0].parentNode.children[1].classList.remove("captured--opponent"); + $('#' + key)[0].parentNode.children[1].classList.add("captured--you"); + //$('#' + key)[0].parentNode.removeAttribute('data-captured'); + $('#' + key)[0].parentNode.setAttribute('data-captured', value.datacaptured); + } else if (value.captured == 'opponent') { + //$('#' + key)[0].parentNode.children[1].classList.remove("captured--you"); + $('#' + key)[0].parentNode.children[1].classList.add("captured--opponent"); + //$('#' + key)[0].parentNode.removeAttribute('data-captured'); + $('#' + key)[0].parentNode.setAttribute('data-captured', value.datacaptured); + } + }); + }, 'json').error(function(jqxhr, status, error) { + console.error("There was a problem retrieving the map data."); + console.log(loadPath); + console.log(status); + console.log(error); + console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); } - /*else { // Inactive country - $('#' + key)[0].classList.remove('active'); - $('#' + key)[0].parentNode.removeAttribute('data-captured'); - $('#' + key)[0].parentNode.children[1].classList.remove("captured--you"); - $('#' + key)[0].parentNode.children[1].classList.remove("captured--opponent"); - }*/ - if (value.captured == 'you') { - //$('#' + key)[0].parentNode.children[1].classList.remove("captured--opponent"); - $('#' + key)[0].parentNode.children[1].classList.add("captured--you"); - //$('#' + key)[0].parentNode.removeAttribute('data-captured'); - $('#' + key)[0].parentNode.setAttribute('data-captured', value.datacaptured); - } else if (value.captured == 'opponent') { - //$('#' + key)[0].parentNode.children[1].classList.remove("captured--you"); - $('#' + key)[0].parentNode.children[1].classList.add("captured--opponent"); - //$('#' + key)[0].parentNode.removeAttribute('data-captured'); - $('#' + key)[0].parentNode.setAttribute('data-captured', value.datacaptured); - } + }).done(function() { + refresh_active_map = false; }); - reload = true; - }, 'json').error(function(jqhxr, status, error) { - console.error("There was a problem retrieving the map data."); - console.log(loadPath); - console.log(status); - console.log(error); - console.error("/error"); - reload = false; - }); + } } /** * clear the map data */ - function clearMapData() { - var loadPath = 'data/map-data.php'; - - return $.get(loadPath, function(data) { - $.each(data, function(key) { - $('#' + key)[0].classList.remove('active'); - $('#' + key)[0].parentNode.removeAttribute('data-captured'); - $('#' + key)[0].parentNode.children[1].classList.remove("captured--you"); - $('#' + key)[0].parentNode.children[1].classList.remove("captured--opponent"); + function clearMapData(force = false) { + if (refresh_active_clear_map === false || force === true) { + refresh_active_clear_map = true; + + var loadPath = 'data/map-data.php'; + + return $.get(loadPath, function(data) { + $.each(data, function(key) { + $('#' + key)[0].classList.remove('active'); + $('#' + key)[0].parentNode.removeAttribute('data-captured'); + $('#' + key)[0].parentNode.children[1].classList.remove("captured--you"); + $('#' + key)[0].parentNode.children[1].classList.remove("captured--opponent"); + }); + }, 'json').error(function(jqxhr, status, error) { + console.error("There was a problem retrieving the map data."); + console.log(loadPath); + console.log(status); + console.log(error); + console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } + }).done(function() { + refresh_active_clear_map = false; }); - reload = true; - }, 'json').error(function(jqhxr, status, error) { - console.error("There was a problem retrieving the map data."); - console.log(loadPath); - console.log(status); - console.log(error); - console.error("/error"); - reload = false; - }); + } } @@ -1626,22 +1777,29 @@ function setupInputListeners() { * @return Deferred * - indicate that this jqxhr request is all done */ - function getCountryData() { - var loadPath = 'data/country-data.php'; - - return $.get(loadPath, function(data) { - FB_CTF.data.COUNTRIES = data; - var df = $.Deferred(); - reload = true; - return df.resolve(FB_CTF.data.COUNTRIES); - }, 'json').error(function(jqxhr, status, error) { - console.error("There was a problem retrieving the game data."); - console.log(loadPath); - console.log(status); - console.log(error); - console.error("/error"); - reload = false; - }); + function getCountryData(force = false) { + if (refresh_active_country === false || force === true) { + refresh_active_country = true; + var loadPath = 'data/country-data.php'; + + return $.get(loadPath, function(data) { + FB_CTF.data.COUNTRIES = data; + var df = $.Deferred(); + return df.resolve(FB_CTF.data.COUNTRIES); + }, 'json').error(function(jqxhr, status, error) { + console.error("There was a problem retrieving the game data."); + console.log(loadPath); + console.log(status); + console.log(error); + console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } + }).done(function() { + refresh_active_country = false; + }); + } } /** @@ -1899,25 +2057,35 @@ function setupInputListeners() { modalId = 'command-line', modalParams = 'p=command-line&modal=command-line', $cmdPromptList, - $cmdResultsList; + $cmdResultsList, + refresh_active_command = false; /** * load the commands data */ function loadCommandsData() { - var loadPath = 'data/command-line.php'; - - return $.get(loadPath, function(data) { - FB_CTF.data.COMMAND = data; - var df = $.Deferred(); - return df.resolve(FB_CTF.data.COMMAND); - }, 'json').error(function(jqhxr, status, error) { - console.error("There was a problem retrieving the commands data."); - console.log(loadPath); - console.log(status); - console.log(error); - console.error("/error"); - }); + if (refresh_active_command === false) { + refresh_active_command = true; + var loadPath = 'data/command-line.php'; + + return $.get(loadPath, function(data) { + FB_CTF.data.COMMAND = data; + var df = $.Deferred(); + return df.resolve(FB_CTF.data.COMMAND); + }, 'json').error(function(jqxhr, status, error) { + console.error("There was a problem retrieving the commands data."); + console.log(loadPath); + console.log(status); + console.log(error); + console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } + }).done(function() { + refresh_active_command = false; + }); + } } /** @@ -2381,9 +2549,15 @@ function setupInputListeners() { }); eventListeners(); } - }, 'json').error(function() { + }, 'json').error(function(jqxhr, status, error) { console.error("There was a problem retrieving the commands."); + console.log(status); + console.log(error); console.error("/error"); + if (jqxhr.status === 500 && jqxhr.getResponseHeader('Error-Redirect') === "true") { + console.log("Redirecting to '/index.php?page=error'"); + window.location.replace('/index.php?page=error'); + } }); }); } @@ -2501,7 +2675,46 @@ function setupInputListeners() { Modal.loadPopup('p=action&modal=account', 'action-account'); }); - // submit account modal + // submit account team name modal + $body.on('click', '.js-trigger-account-team-name-save', function(event) { + event.preventDefault(); + + var team_name = $('.team-name-form input[name=team_name]')[0].value; + var csrf_token = $('.team-name-form input[name=csrf_token]')[0].value; + var team_name_data = { + action: 'set_team_name', + team_name: team_name, + csrf_token: csrf_token + }; + + $.post( + 'index.php?p=game&ajax=true', + team_name_data + ).fail(function() { + // TODO: Make this a modal + console.log('ERROR'); + }).done(function(data) { + var responseData = JSON.parse(data); + if (responseData.result === 'OK') { + console.log('OK'); + $('.team-name-form input[name=team_name]').css("background-color", "#1f7a1f"); + $('.team-name-form span').text('Team Name updated.'); + } else { + console.log('Failed'); + $('.team-name-form input[name=team_name]').css("background-color", "#800000"); + $('.team-name-form span').text('Failed! Please try a different name.'); + } + }); + }); + + $body.on('keypress', '.team-name-form', function(e) { + if (e.keyCode == 13) { + e.preventDefault(); + $('.js-trigger-account-team-name-save').click(); + } + }); + + // submit account livesync modal $body.on('click', '.js-trigger-account-save', function(event) { event.preventDefault(); @@ -2538,22 +2751,33 @@ function setupInputListeners() { }); $body.on('keypress', '.account-link-form', function(e) { - if (e.keyCode == 13) { - e.preventDefault(); - $('.js-trigger-account-save').click(); - } - }); + if (e.keyCode == 13) { + e.preventDefault(); + $('.js-trigger-account-save').click(); + } + }); + + // open Facebook OAuth popup + $body.on('click', '.js-trigger-facebook-oauth', function(event) { + event.preventDefault(); + + var popup = window.open('/data/integration_oauth.php?type=facebook', 'Facebook OAuth', 'height=800,width=800,toolbar=no,scrollbars=1,status=no,location=no,directories=no'); + if (window.focus) { + popup.focus(); + } + return false; + }); // open Google OAuth popup $body.on('click', '.js-trigger-google-oauth', function(event) { - event.preventDefault(); + event.preventDefault(); - var popup = window.open('/data/google_oauth.php', 'Google OAuth', 'height=800,width=800,toolbar=no,scrollbars=1,status=no,location=no,directories=no'); - if (window.focus) { - popup.focus(); - } - return false; - }); + var popup = window.open('/data/integration_oauth.php?type=google', 'Google OAuth', 'height=800,width=800,toolbar=no,scrollbars=1,status=no,location=no,directories=no'); + if (window.focus) { + popup.focus(); + } + return false; + }); // click events $body.on('click', '.click-effect', function() { diff --git a/src/static/js/index.js b/src/static/js/index.js index 142894d1..ea1b5312 100644 --- a/src/static/js/index.js +++ b/src/static/js/index.js @@ -9,8 +9,10 @@ function teamNameFormError() { function teamLoginFormError() { $('.el--text')[0].classList.add('form-error'); + $('.el--text')[1].classList.add('form-error'); $('.fb-form input').on('change', function() { $('.el--text')[0].classList.remove('form-error'); + $('.el--text')[1].classList.remove('form-error'); }); } @@ -149,7 +151,7 @@ module.exports = { if (name && password && !logoInfo.error) { var register_data = { action: 'register_team', - teamname: name, + team_name: name, password: password, logo: logoInfo.logo, isCustomLogo: logoInfo.isCustom, @@ -182,7 +184,7 @@ module.exports = { if (name && password && !logoInfo.error) { var register_data = { action: 'register_names', - teamname: name, + team_name: name, password: password, logo: logoInfo.logo, isCustomLogo: logoInfo.isCustom, @@ -204,7 +206,7 @@ module.exports = { teamParam = 'team_id'; } else { team = $('.fb-form input[name="team_name"]')[0].value; - teamParam = 'teamname'; + teamParam = 'team_name'; } password = verifyTeamPassword(); diff --git a/src/static/svg/map/world.php b/src/static/svg/map/world.php index 6c6a6fb1..226aef86 100644 --- a/src/static/svg/map/world.php +++ b/src/static/svg/map/world.php @@ -2,12 +2,13 @@ require_once ($_SERVER['DOCUMENT_ROOT'].'/../vendor/autoload.php'); -/* HH_IGNORE_ERROR[1002] */ -SessionUtils::sessionStart(); -SessionUtils::enforceLogin(); - -class WorldMapController { +class WorldMapController extends ModuleController { public async function genRender(): Awaitable<:xhp> { + + /* HH_IGNORE_ERROR[1002] */ + SessionUtils::sessionStart(); + SessionUtils::enforceLogin(); + $worldMap = await $this->genRenderWorldMap(); return getId(), SessionUtils::sessionTeam(), false, @@ -106,5 +107,6 @@ class={$path_class} } } +/* HH_IGNORE_ERROR[1002] */ $map = new WorldMapController(); -echo \HH\Asio\join($map->genRender()); +$map->sendRender(); diff --git a/src/xhp/Custombranding.php b/src/xhp/Custombranding.php index e693700e..1b39a799 100644 --- a/src/xhp/Custombranding.php +++ b/src/xhp/Custombranding.php @@ -2,17 +2,15 @@ class :custombranding extends :x:element { category %flow; - attribute - string brandingText, - string brandingLogo; + attribute string brandingText, string brandingLogo; protected string $tagName = 'custombranding'; protected function render(): XHPRoot { return - :brandingLogo}/> -
              + :brandingLogo} /> +
              {$this->:brandingText}
              ; } diff --git a/src/xhp/Fbbranding.php b/src/xhp/Fbbranding.php index 28f6020b..ac57cfa1 100644 --- a/src/xhp/Fbbranding.php +++ b/src/xhp/Fbbranding.php @@ -2,8 +2,7 @@ class :fbbranding extends :x:element { category %flow; - attribute - string brandingText; + attribute string brandingText; protected string $tagName = 'fbbranding'; diff --git a/tests/_files/seed.xml b/tests/_files/seed.xml index b3d08f6a..451f5652 100644 --- a/tests/_files/seed.xml +++ b/tests/_files/seed.xml @@ -153,6 +153,12 @@ 0 description + + 6 + leaderboard_limit + 50 + description +
              id diff --git a/tests/models/ConfigurationTest.php b/tests/models/ConfigurationTest.php index 9df8c735..db337904 100644 --- a/tests/models/ConfigurationTest.php +++ b/tests/models/ConfigurationTest.php @@ -4,7 +4,7 @@ class ConfigurationTest extends FBCTFTest { public function testAllConfiguration(): void { $all = HH\Asio\join(Configuration::genAllConfiguration()); - $this->assertEquals(5, count($all)); + $this->assertEquals(6, count($all)); $c = $all[0]; $this->assertEquals(1, $c->getId()); diff --git a/tests/models/ScoreLogTest.php b/tests/models/ScoreLogTest.php index 793e92dc..b50d7fec 100644 --- a/tests/models/ScoreLogTest.php +++ b/tests/models/ScoreLogTest.php @@ -49,7 +49,7 @@ public function testPreviousScore(): void { $this->assertFalse($previous); } - public function testLogValidScore(): void { + public function testDuplicateLogValidScore(): void { HH\Asio\join(ScoreLog::genLogValidScore( 2, // level 1, // team @@ -57,6 +57,20 @@ public function testLogValidScore(): void { 'base', // any_team )); + $all = HH\Asio\join(ScoreLog::genAllScores()); + $this->assertEquals(2, count($all)); + $s = $all[0]; + $this->assertEquals(10, $s->getPoints()); + } + + public function testLogValidScore(): void { + HH\Asio\join(ScoreLog::genLogValidScore( + 1, // level + 1, // team + 5, // points + 'base', // any_team + )); + $all = HH\Asio\join(ScoreLog::genAllScores()); $this->assertEquals(3, count($all)); $s = $all[0]; diff --git a/tests/models/SessionTest.php b/tests/models/SessionTest.php index 6f969059..e79f947a 100644 --- a/tests/models/SessionTest.php +++ b/tests/models/SessionTest.php @@ -18,7 +18,7 @@ public function testCreate(): void { $all = HH\Asio\join(Session::genAllSessions()); $this->assertEquals(2, count($all)); - $a = $all[0]; + $a = $all[1]; $this->assertEquals(2, $a->getId()); $this->assertEquals('cookie2', $a->getCookie()); $this->assertEquals('data2', $a->getData());