From ac64f553e83d68d5563428f177f3bb571ce4c4cc Mon Sep 17 00:00:00 2001 From: Javier Marcos Date: Mon, 6 Feb 2017 00:15:27 -0800 Subject: [PATCH 01/18] Registration enforcing strong passwords (#442) * Password types in admin * Fully functional password complexity enforcement for registration * lowercase word in text * Adding test for password types regex and fixing all errors for hh_client * Updating outdated schema for tests --- database/schema.sql | 11 +-- database/test_schema.sql | 17 +++-- src/controllers/AdminController.php | 78 ++++++++++++++------ src/controllers/IndexController.php | 6 ++ src/controllers/ajax/IndexAjaxController.php | 20 ++++- src/language/lang_en.php | 6 ++ src/models/Configuration.php | 29 ++++++++ src/models/Logo.php | 2 + src/static/js/admin.js | 10 ++- src/static/js/index.js | 15 +++- src/xhp/Fbbranding.php | 2 +- tests/models/PasswordTypesTest.php | 21 ++++++ 12 files changed, 171 insertions(+), 46 deletions(-) create mode 100644 tests/models/PasswordTypesTest.php diff --git a/database/schema.sql b/database/schema.sql index b076c614..19cd4041 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -207,7 +207,7 @@ 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("password_type", "1", "(Integer) Type of passwords: See password_types"); +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"); INSERT INTO `configuration` (field, value, description) VALUES("language", "en", "(String) Language of the system"); @@ -223,17 +223,18 @@ DROP TABLE IF EXISTS `password_types`; CREATE TABLE `password_types` ( `id` int(11) NOT NULL AUTO_INCREMENT, `field` varchar(100) NOT NULL, + `value` text NOT NULL, `description` text NOT NULL, - `regex` text NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `field` (`field`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; LOCK TABLES `password_types` WRITE; -INSERT INTO `password_types` (field, regex, description) VALUES("1", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[0-9]).*$/", "Length > 8, [a-z] and [0-9]"); -INSERT INTO `password_types` (field, regex, description) VALUES("2", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*$/", "Length > 8, [a-z], [A-Z] and [0-9]"); -INSERT INTO `password_types` (field, regex, description) VALUES("3", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$/", "Length > 8, [a-z], [A-Z], [0-9] and Special chars"); +INSERT INTO `password_types` (field, value, description) VALUES("1", "/.+/", "Length > 0"); +INSERT INTO `password_types` (field, value, description) VALUES("2", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[0-9]).*$/", "Length > 8, [a-z] and [0-9]"); +INSERT INTO `password_types` (field, value, description) VALUES("3", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*$/", "Length > 8, [a-z], [A-Z] and [0-9]"); +INSERT INTO `password_types` (field, value, description) VALUES("4", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[\\W]+).*$/", "Length > 8, [a-z], [A-Z], [0-9] and Special chars"); UNLOCK TABLES; diff --git a/database/test_schema.sql b/database/test_schema.sql index 8ed35e26..963951e0 100644 --- a/database/test_schema.sql +++ b/database/test_schema.sql @@ -118,11 +118,11 @@ CREATE TABLE `teams` ( `active` tinyint(1) NOT NULL DEFAULT 1, `name` text NOT NULL, `password_hash` text NOT NULL, - `points` int(11) NOT NULL, + `points` int(11) NOT NULL DEFAULT 0, `last_score` timestamp NOT NULL, `logo` text NOT NULL, - `admin` tinyint(1) NOT NULL, - `protected` tinyint(1) NOT NULL, + `admin` tinyint(1) NOT NULL DEFAULT 0, + `protected` tinyint(1) NOT NULL DEFAULT 0, `visible` tinyint(1) NOT NULL DEFAULT 1, `created_ts` timestamp NOT NULL DEFAULT 0, PRIMARY KEY (`id`) @@ -207,7 +207,7 @@ 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("password_type", "1", "(Integer) Type of passwords: See password_types"); +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"); INSERT INTO `configuration` (field, value, description) VALUES("language", "en", "(String) Language of the system"); @@ -223,17 +223,18 @@ DROP TABLE IF EXISTS `password_types`; CREATE TABLE `password_types` ( `id` int(11) NOT NULL AUTO_INCREMENT, `field` varchar(100) NOT NULL, + `value` text NOT NULL, `description` text NOT NULL, - `regex` text NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `field` (`field`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; LOCK TABLES `password_types` WRITE; -INSERT INTO `password_types` (field, regex, description) VALUES("1", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[0-9]).*$/", "Length > 8, [a-z] and [0-9]"); -INSERT INTO `password_types` (field, regex, description) VALUES("2", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*$/", "Length > 8, [a-z], [A-Z] and [0-9]"); -INSERT INTO `password_types` (field, regex, description) VALUES("3", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$/", "Length > 8, [a-z], [A-Z], [0-9] and Special chars"); +INSERT INTO `password_types` (field, value, description) VALUES("1", "/.+/", "Length > 0"); +INSERT INTO `password_types` (field, value, description) VALUES("2", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[0-9]).*$/", "Length > 8, [a-z] and [0-9]"); +INSERT INTO `password_types` (field, value, description) VALUES("3", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*$/", "Length > 8, [a-z], [A-Z] and [0-9]"); +INSERT INTO `password_types` (field, value, description) VALUES("4", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[\\W]+).*$/", "Length > 8, [a-z], [A-Z], [0-9] and Special chars"); UNLOCK TABLES; diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index 3ffc0abc..35be1b71 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -149,6 +149,25 @@ class="fb--conf--registration_type" return $select; } + // TODO: Translate password types + private async function genStrongPasswordsSelect(): Awaitable<:xhp> { + $types = await Configuration::genAllPasswordTypes(); + $config = await Configuration::genCurrentPasswordType(); + $select = ; + foreach ($types as $type) { + $select->appendChild( + + ); + } + + return $select; + } + private async function genConfigurationDurationSelect(): Awaitable<:xhp> { $config = await Configuration::gen('game_duration_unit'); $duration_unit = $config->getValue(); @@ -408,6 +427,7 @@ class="fb-cta cta--yellow" 'configuration_duration_select' => $this->genConfigurationDurationSelect(), 'language_select' => $this->genLanguageSelect(), + 'password_types_select' => $this->genStrongPasswordsSelect(), }; $results = await \HH\Asio\m($awaitables); @@ -415,6 +435,18 @@ class="fb-cta cta--yellow" $configuration_duration_select = $results['configuration_duration_select']; $language_select = $results['language_select']; + $password_types_select = $results['password_types_select']; + + $login_strongpasswords = await Configuration::gen('login_strongpasswords'); + if ($login_strongpasswords->getValue() === '0') { // Strong passwords are not enforced + $strong_passwords =
; + } else { + $strong_passwords = +
+ + {$password_types_select} +
; + } return
@@ -519,56 +551,59 @@ class="fb-cta cta--yellow"
-
+
- +
-
-
+
- +
-
+
+ {$strong_passwords} +
@@ -2852,7 +2887,8 @@ class={$highlighted_color} if (count($failures) > 0) { $failures_tbody = ; foreach ($failures as $failure) { - if (!Level::genCheckStatus($failure->getLevelId())) { + $level_genCheckStatus = await Level::genCheckStatus($failure->getLevelId()); + if (!$level_genCheckStatus) { continue; } $level = await Level::gen($failure->getLevelId()); diff --git a/src/controllers/IndexController.php b/src/controllers/IndexController.php index 0982724d..548be461 100644 --- a/src/controllers/IndexController.php +++ b/src/controllers/IndexController.php @@ -345,6 +345,7 @@ class="fb-main page--team-registration full-height fb-scroll"> name="teamname" type="text" maxlength={20} + autofocus={true} /> {$ldap_domain_suffix}
@@ -451,6 +452,7 @@ class="fb-main page--registration full-height fb-scroll"> name="teamname" type="text" maxlength={20} + autofocus={true} /> {$ldap_domain_suffix}
@@ -458,6 +460,10 @@ class="fb-main page--registration full-height fb-scroll"> +
+ +
{tr('Password is too simple')}
+
{$token_field}
diff --git a/src/controllers/ajax/IndexAjaxController.php b/src/controllers/ajax/IndexAjaxController.php index 3843b957..e5e444eb 100644 --- a/src/controllers/ajax/IndexAjaxController.php +++ b/src/controllers/ajax/IndexAjaxController.php @@ -115,8 +115,18 @@ protected function getActions(): array { return Utils::error_response('Registration failed', 'registration'); } + // Check if strongs passwords are enforced + $login_strongpasswords = await Configuration::gen('login_strongpasswords'); + if ($login_strongpasswords->getValue() !== '0') { + $password_type = await Configuration::genCurrentPasswordType(); + if (!preg_match(strval($password_type->getValue()), $password)) { + return Utils::error_response('Password too simple', 'registration'); + } + } + // Check if ldap is enabled and verify credentials if successful $ldap = await Configuration::gen('ldap'); + $ldap_password = ''; if ($ldap->getValue() === '1') { // Get server information from configuration $ldap_server = await Configuration::gen('ldap_server'); @@ -145,10 +155,10 @@ protected function getActions(): array { // This will help avoid leaking users ldap passwords if the server's database // is compromised. $ldap_password = $password; - $password = gmp_strval( + $password = strval(gmp_strval( gmp_init(bin2hex(openssl_random_pseudo_bytes(16)), 16), 62, - ); + )); } // Check if tokenized registration is enabled @@ -205,9 +215,11 @@ protected function getActions(): array { await Token::genUse($token, $team_id); } // Login the team - if ($ldap->getValue() === '1') - return await $this->genLoginTeam($team_id, $ldap_password); else + if ($ldap->getValue() === '1') { + return await $this->genLoginTeam($team_id, $ldap_password); + } else { return await $this->genLoginTeam($team_id, $password); + } } else { return Utils::error_response('Registration failed', 'registration'); } diff --git a/src/language/lang_en.php b/src/language/lang_en.php index 4a572053..d8aad5c1 100644 --- a/src/language/lang_en.php +++ b/src/language/lang_en.php @@ -85,6 +85,12 @@ 'Password', 'Choose an Emblem' => 'Choose an Emblem', + 'or Upload your own' => + 'or Upload your own', + 'Clear your custom emblem to use a default emblem.' => + 'Clear your custom emblem to use a default emblem.', + 'Password is too simple' => + 'Password is too simple', 'Sign Up' => 'Sign Up', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => diff --git a/src/models/Configuration.php b/src/models/Configuration.php index 5af97b4a..2317bd34 100644 --- a/src/models/Configuration.php +++ b/src/models/Configuration.php @@ -78,6 +78,35 @@ public function getDescription(): string { return intval(idx(firstx($result->mapRows()), 'COUNT(*)')) > 0; } + // All the password types. + public static async function genAllPasswordTypes( + ): Awaitable> { + $db = await self::genDb(); + $result = await $db->queryf('SELECT * FROM password_types'); + + $types = array(); + foreach ($result->mapRows() as $row) { + $types[] = self::configurationFromRow($row->toArray()); + } + + return $types; + } + + // Current password type. + 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' + ); + + invariant($db_result->numRows() === 1, 'Expected exactly one result'); + $result = firstx($db_result->mapRows())->toArray(); + + return self::configurationFromRow($result); + } + // All the configuration. public static async function genAllConfiguration( ): Awaitable> { diff --git a/src/models/Logo.php b/src/models/Logo.php index bd7bcc54..599fac09 100644 --- a/src/models/Logo.php +++ b/src/models/Logo.php @@ -269,6 +269,8 @@ private static function logoFromRow(Map $row): Logo { // Get image properties and verify mimetype $base64_data = str_replace(' ', '+', $base64_data); $binary_data = base64_decode(str_replace(' ', '+', $base64_data)); + /* HH_IGNORE_ERROR[2049] */ + /* HH_IGNORE_ERROR[4107] */ $image_info = getimagesizefromstring($binary_data); $mimetype = $image_info[2]; diff --git a/src/static/js/admin.js b/src/static/js/admin.js index 8570b449..17d13315 100644 --- a/src/static/js/admin.js +++ b/src/static/js/admin.js @@ -909,7 +909,10 @@ function toggleConfiguration(radio_id) { field: radio_action, value: action_value }; - if (radio_action) { + var refresh_fields = ['login_strongpasswords']; + if (refresh_fields.indexOf(radio_action) !== -1) { + sendAdminRequest(toggle_data, true); + } else { sendAdminRequest(toggle_data, false); } } @@ -920,9 +923,10 @@ function changeConfiguration(field, value) { field: field, value: value }; - if (field === 'registration_type' || field === 'language') { + var refresh_fields = ['registration_type', 'language']; + if (refresh_fields.indexOf(field) !== -1) { sendAdminRequest(conf_data, true); - }else { + } else { sendAdminRequest(conf_data, false); } } diff --git a/src/static/js/index.js b/src/static/js/index.js index 32e04954..289fa87f 100644 --- a/src/static/js/index.js +++ b/src/static/js/index.js @@ -7,10 +7,16 @@ function teamNameFormError() { }); } -function teamPasswordFormError() { +function teamPasswordFormError(toosimple) { $('.el--text')[1].classList.add('form-error'); + if (toosimple) { + $('#password_error')[0].classList.remove('completely-hidden'); + } $('.fb-form input[name="password"]').on('change', function() { $('.el--text')[1].classList.remove('form-error'); + if (toosimple) { + $('#password_error')[0].classList.add('completely-hidden'); + } }); } @@ -109,9 +115,10 @@ function sendIndexRequest(request_data) { goToPage(responseData.redirect); } else { // TODO: Make this a modal - console.log('Failed'); - teamNameFormError(); - teamPasswordFormError(); + verifyTeamName('register'); + if (responseData.message === 'Password too simple') { + teamPasswordFormError(true); + } teamTokenFormError(); } }); diff --git a/src/xhp/Fbbranding.php b/src/xhp/Fbbranding.php index 158e1c95..fa9d18fa 100644 --- a/src/xhp/Fbbranding.php +++ b/src/xhp/Fbbranding.php @@ -11,7 +11,7 @@ protected function render(): XHPRoot { - {tr('Powered By Facebook')} + {' '}{tr('Powered By Facebook')} ; } } diff --git a/tests/models/PasswordTypesTest.php b/tests/models/PasswordTypesTest.php new file mode 100644 index 00000000..3680eefa --- /dev/null +++ b/tests/models/PasswordTypesTest.php @@ -0,0 +1,21 @@ +assertEquals(4, count($all)); + + $p = $all[0]; + $this->assertTrue((bool)preg_match($p->getValue(), 'a')); + + $p = $all[1]; + $this->assertTrue((bool)preg_match($p->getValue(), 'password1')); + + $p = $all[2]; + $this->assertTrue((bool)preg_match($p->getValue(), 'Password1')); + + $p = $all[3]; + $this->assertTrue((bool)preg_match($p->getValue(), 'Pas$word1')); + } +} From 081062cf8205d90f2dcad8d288ffd1dfa31460a7 Mon Sep 17 00:00:00 2001 From: Javier Marcos Date: Sun, 12 Feb 2017 18:03:42 -0800 Subject: [PATCH 02/18] Custom branding for icon and text (#448) * Custom branding for icon and text * Replace async calls branding xhp by attributes * Use genRenderBranding in genRenderMobilePage and combine awaitables --- database/schema.sql | 3 + src/controllers/AdminController.php | 95 ++++++++++++++++++-- src/controllers/Controller.php | 25 ++++++ src/controllers/GameboardController.php | 14 +-- src/controllers/IndexController.php | 29 +++--- src/controllers/ViewModeController.php | 14 +-- src/controllers/ajax/AdminAjaxController.php | 13 +++ src/language/lang_en.php | 14 ++- src/language/lang_es.php | 12 ++- src/models/Logo.php | 8 +- src/static/css/scss/_admin.scss | 4 + src/static/css/scss/_icons.scss | 4 + src/static/js/admin.js | 39 +++++++- src/static/js/fb-ctf.js | 1 - src/xhp/Custombranding.php | 19 ++++ src/xhp/Fbbranding.php | 4 +- 16 files changed, 260 insertions(+), 38 deletions(-) create mode 100644 src/xhp/Custombranding.php diff --git a/database/schema.sql b/database/schema.sql index 19cd4041..dd30bbe6 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -211,6 +211,9 @@ INSERT INTO `configuration` (field, value, description) VALUES("password_type", 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"); INSERT INTO `configuration` (field, value, description) VALUES("language", "en", "(String) Language of the system"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_logo", "0", "(Boolean) Custom branding logo"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_text", "Powered By Facebook", "(String) Custom branding text"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_logo_image", "static/img/favicon.png", "(String) Custom logo image file"); UNLOCK TABLES; -- diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index 35be1b71..5660f824 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -311,6 +311,9 @@ class="fb-cta cta--yellow" 'bases_cycle' => Configuration::gen('bases_cycle'), 'start_ts' => Configuration::gen('start_ts'), 'end_ts' => Configuration::gen('end_ts'), + 'custom_logo' => Configuration::gen('custom_logo'), + 'custom_text' => Configuration::gen('custom_text'), + 'custom_logo_image' => Configuration::gen('custom_logo_image'), }; $results = await \HH\Asio\m($awaitables); @@ -335,6 +338,9 @@ class="fb-cta cta--yellow" $bases_cycle = $results['bases_cycle']; $start_ts = $results['start_ts']; $end_ts = $results['end_ts']; + $custom_logo = $results['custom_logo']; + $custom_text = $results['custom_text']; + $custom_logo_image = $results['custom_logo_image']; $registration_on = $registration->getValue() === '1'; $registration_off = $registration->getValue() === '0'; @@ -354,6 +360,8 @@ class="fb-cta cta--yellow" $gameboard_off = $gameboard->getValue() === '0'; $timer_on = $timer->getValue() === '1'; $timer_off = $timer->getValue() === '0'; + $custom_logo_on = $custom_logo->getValue() === '1'; + $custom_logo_off = $custom_logo->getValue() === '0'; $game_start_array = array(); if ($start_ts->getValue() !== '0' && $start_ts->getValue() !== 'NaN') { @@ -437,7 +445,6 @@ class="fb-cta cta--yellow" $language_select = $results['language_select']; $password_types_select = $results['password_types_select']; - $login_strongpasswords = await Configuration::gen('login_strongpasswords'); if ($login_strongpasswords->getValue() === '0') { // Strong passwords are not enforced $strong_passwords =
; } else { @@ -448,6 +455,33 @@ class="fb-cta cta--yellow"
; } + if ($custom_logo->getValue() === '0') { // Custom branding is not enabled + $custom_logo_xhp =
; + } else { + $custom_logo_xhp = +
+ + getValue()} + /> +
+
+ + {tr('Change')} + +
+ +
; + } + return
@@ -914,11 +948,59 @@ class="fb-cta cta--yellow"
-

{tr('Language')}

+

{tr('Internationalization')}

+
+
+
+
+ + {$language_select} +
+
+
+
+
+
+

{tr('Branding')}

-
-
- {$language_select} +
+
+
+ +
+ + + + +
+
+
+
+ {$custom_logo_xhp} +
+
+
+ + getValue()} + /> +
@@ -3718,6 +3800,7 @@ public function renderMainContent(): :xhp { {tr('Begin Game')} ; } + $branding_xhp = await $this->genRenderBranding(); return
@@ -3790,7 +3873,7 @@ public function renderMainContent(): :xhp { {tr('Gameboard')} {tr('Logout')} - + {$branding_xhp}
; } diff --git a/src/controllers/Controller.php b/src/controllers/Controller.php index 53ae6084..84974f08 100644 --- a/src/controllers/Controller.php +++ b/src/controllers/Controller.php @@ -7,6 +7,31 @@ abstract protected function getPages(): array; abstract protected function genRenderBody(string $page): Awaitable<:xhp>; + public async function genRenderBranding(): Awaitable<:xhp> { + $awaitables = Map { + 'custom_logo' => Configuration::gen('custom_logo'), + 'custom_text' => Configuration::gen('custom_text'), + 'custom_logo_image' => Configuration::gen('custom_logo_image'), + }; + $results = await \HH\Asio\m($awaitables); + $branding = $results['custom_logo']; + $custom_text = $results['custom_text']; + if ($branding->getValue() === '0') { + $branding_xhp = + getValue()))} + />; + } else { + $custom_logo_image = $results['custom_logo_image']; + $branding_xhp = + getValue())} + brandingLogo={strval($custom_logo_image->getValue())} + />; + } + return $branding_xhp; + } + public async function genRender(): Awaitable<:xhp> { $page = $this->processRequest(); $body = await $this->genRenderBody($page); diff --git a/src/controllers/GameboardController.php b/src/controllers/GameboardController.php index e194ca40..0d6cd0b1 100644 --- a/src/controllers/GameboardController.php +++ b/src/controllers/GameboardController.php @@ -23,12 +23,13 @@ protected function getPages(): array { return array('main', 'viewmode'); } - public function renderMainContent(): :xhp { + public async function genRenderMainContent(): Awaitable<:xhp> { if (SessionUtils::sessionAdmin()) { $admin_link =
  • {tr('Admin')}
  • ; } else { $admin_link = null; } + $branding_gen = await $this->genRenderBranding(); return
    @@ -58,7 +59,7 @@ public function renderMainContent(): :xhp { @@ -137,19 +138,20 @@ class="module--outer-right"
    ; } - public function renderPage(string $page): :xhp { + public async function genRenderPage(string $page): Awaitable<:xhp> { switch ($page) { case 'main': - return $this->renderMainContent(); + return await $this->genRenderMainContent(); break; default: - return $this->renderMainContent(); + return await $this->genRenderMainContent(); break; } } <<__Override>> public async function genRenderBody(string $page): Awaitable<:xhp> { + $rendered_page = await $this->genRenderPage($page); return
    - {$this->renderPage($page)} + {$rendered_page}
    ; diff --git a/src/controllers/IndexController.php b/src/controllers/IndexController.php index 548be461..d7f1920f 100644 --- a/src/controllers/IndexController.php +++ b/src/controllers/IndexController.php @@ -732,7 +732,8 @@ public function renderErrorPage(): :xhp { ; } - public function renderMobilePage(): :xhp { + public async function genRenderMobilePage(): Awaitable<:xhp> { + $branding_xhp = await $this->genRenderBranding(); return
    - + {$branding_xhp}
    ; } - public function renderMainNav(): :xhp { + public async function genRenderMainNav(): Awaitable<:xhp> { if (SessionUtils::sessionActive()) { $right_nav = ; + $branding_gen = await $this->genRenderBranding(); + $branding = + ; return ; } @@ -826,7 +830,7 @@ public function renderMainNav(): :xhp { case 'error': return $this->renderErrorPage(); case 'mobile': - return $this->renderMobilePage(); + return await $this->genRenderMobilePage(); case 'login': return await $this->genRenderLoginContent(); case 'registration': @@ -847,11 +851,12 @@ public function renderMainNav(): :xhp { <<__Override>> public async function genRenderBody(string $page): Awaitable<:xhp> { $rendered_page = await $this->genRenderPage($page); + $rendered_nav = await $this->genRenderMainNav(); return
    -
    {$this->renderMainNav()}
    +
    {$rendered_nav}
    {$rendered_page}
    diff --git a/src/controllers/ViewModeController.php b/src/controllers/ViewModeController.php index d2459000..172e92ac 100644 --- a/src/controllers/ViewModeController.php +++ b/src/controllers/ViewModeController.php @@ -23,7 +23,8 @@ protected function getPages(): array { return array('main'); } - public function renderMainContent(): :xhp { + public async function genRenderMainContent(): Awaitable<:xhp> { + $branding_gen = await $this->genRenderBranding(); return
    @@ -31,7 +32,7 @@ public function renderMainContent(): :xhp { @@ -58,24 +59,25 @@ class="module--outer-right active"
    ; } - public function renderPage(string $page): :xhp { + public async function genRenderPage(string $page): Awaitable<:xhp> { switch ($page) { case 'main': - return $this->renderMainContent(); + return await $this->genRenderMainContent(); break; default: - return $this->renderMainContent(); + return await $this->genRenderMainContent(); break; } } <<__Override>> public async function genRenderBody(string $page): Awaitable<:xhp> { + $rendered_page = await $this->genRenderPage($page); return
    - {$this->renderPage($page)} + {$rendered_page}
    ; diff --git a/src/controllers/ajax/AdminAjaxController.php b/src/controllers/ajax/AdminAjaxController.php index f9461961..9f182190 100644 --- a/src/controllers/ajax/AdminAjaxController.php +++ b/src/controllers/ajax/AdminAjaxController.php @@ -29,6 +29,10 @@ protected function getFilters(): array { 'filter' => FILTER_VALIDATE_REGEXP, 'options' => array('regexp' => '/^[\w-]+$/'), ), + 'logo_b64' => array( + 'filter' => FILTER_VALIDATE_REGEXP, + 'options' => array('regexp' => '/^[\w+-\/]+={0,2}$/'), + ), 'entity_id' => FILTER_VALIDATE_INT, 'attachment_id' => FILTER_VALIDATE_INT, 'filename' => array( @@ -117,6 +121,7 @@ protected function getActions(): array { 'delete_link', 'begin_game', 'change_configuration', + 'change_custom_logo', 'create_announcement', 'delete_announcement', 'create_tokens', @@ -404,6 +409,14 @@ protected function getActions(): array { } else { return Utils::error_response('Invalid configuration', 'admin'); } + case 'change_custom_logo': + $logo = must_have_string($params, 'logo_b64'); + $custom_logo = await Logo::genCreateCustom($logo, true); + if ($custom_logo) { + return Utils::ok_response('Success', 'admin'); + } else { + return Utils::error_response('Error changing logo', 'admin'); + } case 'create_announcement': await Announcement::genCreate( must_have_string($params, 'announcement'), diff --git a/src/language/lang_en.php b/src/language/lang_en.php index d8aad5c1..77563863 100644 --- a/src/language/lang_en.php +++ b/src/language/lang_en.php @@ -85,8 +85,8 @@ 'Password', 'Choose an Emblem' => 'Choose an Emblem', - 'or Upload your own' => - 'or Upload your own', + 'or upload your own' => + 'or upload your own', 'Clear your custom emblem to use a default emblem.' => 'Clear your custom emblem to use a default emblem.', 'Password is too simple' => @@ -235,8 +235,18 @@ 'Begin Time', 'Expected End Time' => 'Expected End Time', + 'Internationalization' => + 'Internationalization', 'Language' => 'Language', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Custom Logo', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Custom Text', 'DELETE' => 'DELETE', 'Delete' => diff --git a/src/language/lang_es.php b/src/language/lang_es.php index 9d6e19b0..5903087b 100644 --- a/src/language/lang_es.php +++ b/src/language/lang_es.php @@ -229,8 +229,18 @@ 'Tiempo de Inicio', 'Expected End Time' => 'Tiempo de Finalización Esperada', + 'Internationalization' => + 'Internacionalización', 'Language' => - 'Lenguaje', + 'Idioma', + 'Branding' => + 'Personalización', + 'Custom Logo' => + 'Logo Personalizado', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Texto Personalizado', 'DELETE' => 'BORRAR', 'Delete' => diff --git a/src/models/Logo.php b/src/models/Logo.php index 599fac09..a4f69ec5 100644 --- a/src/models/Logo.php +++ b/src/models/Logo.php @@ -252,6 +252,7 @@ private static function logoFromRow(Map $row): Logo { // Create custom logo public static async function genCreateCustom( string $base64_data, + bool $branding = false, ): Awaitable { // Check image size $image_size_bytes = strlen($base64_data) * self::BASE64_BYTES_PER_CHAR; @@ -297,8 +298,6 @@ private static function logoFromRow(Map $row): Logo { ); } - $db = await self::genDb(); - $used = true; $enabled = true; $protected = false; @@ -312,6 +311,11 @@ private static function logoFromRow(Map $row): Logo { $filepath, ); + // If created logo is for branding, set configuration value + if ($branding) { + await Configuration::genUpdate('custom_logo_image', $logo->getLogo()); + } + // Return newly created logo_id return $logo; } diff --git a/src/static/css/scss/_admin.scss b/src/static/css/scss/_admin.scss index 69f53f91..e887c034 100644 --- a/src/static/css/scss/_admin.scss +++ b/src/static/css/scss/_admin.scss @@ -139,6 +139,10 @@ General Admin } } + #custom-logo-input { + display: none; + } + input:not(:checked) + label { color: $teal-blue; } diff --git a/src/static/css/scss/_icons.scss b/src/static/css/scss/_icons.scss index 96cc5580..34f3300c 100644 --- a/src/static/css/scss/_icons.scss +++ b/src/static/css/scss/_icons.scss @@ -9,6 +9,10 @@ vertical-align: middle; } +.icon-text { + vertical-align: middle; +} + .has-icon { vertical-align: middle; } diff --git a/src/static/js/admin.js b/src/static/js/admin.js index 17d13315..362a7551 100644 --- a/src/static/js/admin.js +++ b/src/static/js/admin.js @@ -909,7 +909,7 @@ function toggleConfiguration(radio_id) { field: radio_action, value: action_value }; - var refresh_fields = ['login_strongpasswords']; + var refresh_fields = ['login_strongpasswords', 'custom_logo']; if (refresh_fields.indexOf(radio_action) !== -1) { sendAdminRequest(toggle_data, true); } else { @@ -1393,5 +1393,42 @@ module.exports = { }); }); + // custom logo file selector + var $customLogoInput = $('#custom-logo-input'); + var $customLogoImage = $('#custom-logo-image'); + $('#custom-logo-link').on('click', function() { + $customLogoInput.trigger('click'); + }); + // on file input change, set image + $customLogoInput.change(function() { + var input = this; + if (input.files && input.files[0]) { + if (input.files[0].size > (1000*1024)) { + alert('Please upload an image less than 1000KB!'); + return; + } + + var reader = new FileReader(); + + reader.onload = function (e) { + $customLogoImage.attr('src', e.target.result); + var rawImageData = e.target.result; + var filetypeBeginIdx = rawImageData.indexOf('/') + 1; + var filetypeEndIdx = rawImageData.indexOf(';'); + var filetype = rawImageData.substring(filetypeBeginIdx, filetypeEndIdx); + var base64 = rawImageData.substring(rawImageData.indexOf(',') + 1); + var logo_data = { + action: 'change_custom_logo', + logoType: filetype, + logo_b64: base64 + }; + sendAdminRequest(logo_data, true); + }; + + reader.readAsDataURL(input.files[0]); + + } + }); + } }; diff --git a/src/static/js/fb-ctf.js b/src/static/js/fb-ctf.js index 02d99553..bdc1ca4d 100644 --- a/src/static/js/fb-ctf.js +++ b/src/static/js/fb-ctf.js @@ -2511,7 +2511,6 @@ function setupInputListeners() { }); // on file input change, set image preview and emblem carousel notice $customEmblemInput.change(function() { -console.log('foo'); var input = this; if (input.files && input.files[0]) { if (input.files[0].size > (1000*1024)) { diff --git a/src/xhp/Custombranding.php b/src/xhp/Custombranding.php new file mode 100644 index 00000000..e693700e --- /dev/null +++ b/src/xhp/Custombranding.php @@ -0,0 +1,19 @@ + + :brandingLogo}/> +
    + {$this->:brandingText} + ; + } +} diff --git a/src/xhp/Fbbranding.php b/src/xhp/Fbbranding.php index fa9d18fa..28f6020b 100644 --- a/src/xhp/Fbbranding.php +++ b/src/xhp/Fbbranding.php @@ -2,6 +2,8 @@ class :fbbranding extends :x:element { category %flow; + attribute + string brandingText; protected string $tagName = 'fbbranding'; @@ -11,7 +13,7 @@ protected function render(): XHPRoot { - {' '}{tr('Powered By Facebook')} + {' '}{$this->:brandingText} ; } } From 25c174870fec0e020afabe9f1a77439300b3a05c Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Mon, 5 Jun 2017 18:00:01 -0400 Subject: [PATCH 03/18] Merge of /master into /dev - Baseline for Development (#509) * add hindi translation * added hindi translation * Update lang_hi.php * Error Checking During Build Tests (#452) * Error Checking During Build Tests * Execute hh_client during build tests. * Currently the PHP built-in getimagesizefromstring function is not in the HHVM upstream hhi, and therefore generates an error. In the future, once getimagesizefromstring is added upstream, we should use the hh_client exit status. * * Readded execute permissions to the script. * HHVM/Hack Typing Error Fixes (#450) * HHVM/Hack Typing Error Fixes * Fixed a few HHVM/Hack typing and strict compliance issues. * This is necessary before hh_client can run and be enforced during the build process. (See comments on #435) * * Updated formatting. * Require bxslider version 4.2.6 (Fixes #455) (#458) * This resolves a current build error #455. * bxslider was updated from 4.2.6 to 4.2.7 on February 14th. Previously FBCTF allowed for a near match to 4.2.6. However, FBCTF fails to build with 4.2.7. During the installation, process grunt failed to build the browserify javascript. * Fixed Syntax Errors in Hindi Language (Fixes Build Errors) (#460) * Fixed minor syntax error due to character encoding. * This will ensure the project builds (no Hack errors). * Automated Game Start and Stop (#449) * Automated Game Start and Stop * Games will automatically start and stop at their scheduled times. Administrators can still manually start or stop a game regardless of the configured schedule. * Both Control::genAutoBegin() and Control::genAutoEnd() were added to check the current time against the scheduled start or stop time and perform the relevant action (Control::genBegin or Control::getEnd). * Control::genAutoRun() checks the current game status and determine if the game should be starting or ending, calling the appropriate function (Control::genAutoBegin or Control::getAutoEnd) and is exclusively used in the new autorun.php script. * Control::genRunAutoRunScript() runs the new autorun.php script, ensuring the script is not already running before starting a new copy. * The Router class was updated to include a call to Control::genRunAutoRunScript(), this ensures the script is always running. This script status check, and execution when needed, only takes place on a full page load. * The autorun.php script runs Control::genAutoRun() and sleeps up to 30 seconds. * If the game is scheduled to start or stop within 30 seconds, the script will sleep for the necessary amount of time. * Games will always start with at most a 29-second difference from the scheduled time. This discrepancy can only take place if the schedule is changed within 30 seconds of the previously scheduled time. Otherwise, the execution will happen at the scheduled time. * This automation is self-contained and requires no additional dependencies or external services (like cron, etc.). * * Allow administrators to define the cycle time (in seconds) for the autorun process. This time will be used for the sliding sleep. * * Added sanitization to the autorun script path/file. * Attachments and Links Import/Export, Database Restore, and Control Cleanup (#451) * Attachments and Links Import/Export, Database Restore, and Control Cleanup * Attachments can now be exported and imported. On export, attachments are downloaded into a Tar Gzip and securely extracted on import. * Links and Attachments data is now provided within the Levels export. Users must import both the Level data and the Attachment files to restore the levels with attachments. * A database restore option has been added which utilizes the backed up database content. This overwrites all data in the database. * The Control page has been reorganized to align the various functionality better. * Memcached flushing has been added to all relevant data imports. * Error handling has been added to the various import functions. * * Removed getter function for the Attachment constant. * Switched double quotes with single quotes. * Update README.md * Update README.md * Update README.md * Update README.md * Live Sync API (#459) * Live Sync API * Introducing the Live Sync API. * The Live Sync API allows administrators to import game activity in near-real-time. Users can link their accounts on one or more FBCTF platform instances and their scores will be automatically imported into the systems that have been linked. * The primary use-case revolves around event aggregation across multiple FBCTF instances. Event organizers can now separate FBCTF instances and combine scores into one global scoreboard. * The Live Sync API will import Levels, Categories, Scoring Events, and Hint Usage. Scores are automatically calculated, and bonuses are updated to ensure accurate scoring across linked FBCTF instances. * Administrators determine which systems, if any, are linked. * Users must link their account in order for their activity to be synced. * The UI/UX of FBCTF has been updated to include a mechanism for users to configure their Live Sync credentials. * Users cannot obtain hints or capture levels on the importing system. * The API is JSON based and the schema is generalized so that it can leveraged by other platforms or external processes. So data can be synced from non-FBCTF platforms. * The importing script will automatically handle country conflicts (if two systems have the same country selected for a level). * USER GUIDE (Documentation): * Users must first have an account on all FBCTF instances they wish to link. * The user must then login and access the game board. * From the navigation menu, the user should select "Account." * The user must then set a Live Sync username and password. The Live Sync username and password must not be their login credentials. In fact, users are prohibited from using their account password as their Live Sync password. * The user would repeat the above steps for each FBCTF instance they wish to link. The Live Sync credentials must be the same on each FBCTF instance or their accounts will not be linked. * ADMIN GUIDE (Documentation): * The admin is free to sync as many platforms as their desire. Additionally the admin may import from as many API sources as their desire. * The admin will need to launch the "live import" script, on any importing systems, from the command-line: * `hhvm -vRepo.Central.Path=/var/run/hhvm/.hhvm.hhbc_liveimport /var/www/fbctf/src/scripts/liveimport.php ` * Disabling of the SSL Verification and Debugging are both optional. The URL(s) and Sleep time are required arguments. * EXAMPLE: `hhvm -vRepo.Central.Path=/var/run/hhvm/.hhvm.hhbc_liveimport /var/www/fbctf/src/scripts/liveimport.php "https://10.10.10.101/data/livesync.php https://10.10.10.102/data/livesync.php https://10.10.10.103/other/platform/api" 300 true true` * API SCHEMA (JSON): * JSON: [{"active":true,"type":"flag","title":"Example Level 1","description":"This is the first example level.","entity_iso_code":"TJ","category":"None","points":100,"bonus":30,"bonus_dec":10,"penalty":10,"teams":{"fbctf:user1:$2y$12$a1T4KyqqxADi3YIJ7M2sf.VoSHz6qMBx.zrxAIvZnD8de95EsLeny":{"timestamp":"2017-02-17 02:20:22","capture":true,"hint":false}}}] * Explained (Formatted output for readability - the actually data must be in valid JSON format): [0] => Array ( [active] => 1 // Level Status (Enabled/Disabled) [type] => flag // Level Type (Flag or Quiz) [title] => Example Level 1 // Level Name [description] => This is the first example level. // Level Description [entity_iso_code] => US // Country Code (Mapping) [category] => None // Level Category [points] => 100 // Points [bonus] => 30 // Bonus Points [bonus_dec] => 10 // Bonus Point Decrement [penalty] => 0 // Hint Cost [teams] => Array ( [fbctf:user3:$2y$12$GIR7V0Q2OMDv8cTTOnzKVpGYgR4.pWTsPRHtZ3yenKZ9JxOabx4m2] => Array // Live Sync Type, Live Sync Username, Live Sync Key (Hash) ( [timestamp] => 2017-02-17 01:09:24 // Activity Timestamp [capture] => 1 // Capture Status [hint] => // Hint Used ) ) ) * Example (Formatted output for readability - the actually data must be in valid JSON format): Array ( [0] => Array ( [active] => 1 [type] => flag [title] => Example Level 1 [description] => This is the first example level. [entity_iso_code] => US [category] => None [points] => 100 [bonus] => 30 [bonus_dec] => 10 [penalty] => 0 [teams] => Array ( ) ) [1] => Array ( [active] => 1 [type] => flag [title] => Example Level 2 [description] => This is the second example level. [entity_iso_code] => OM [category] => None [points] => 100 [bonus] => 30 [bonus_dec] => 10 [penalty] => 0 [teams] => Array ( [fbctf:user1:$2y$12$n.VmlNNwxmZ/OkGGuhVhFeX0VExAgjoaYzyetLCIemSXN/yxWXLyO] => Array ( [timestamp] => 2017-02-17 01:01:49 [capture] => 1 [hint] => 1 ) [fbctf:user2:$2y$12$GIDv8cR7V0nzKVpQ2OMTTOGYgR4.pWTxOPRH9abtsJZ3yenKZx4m2] => Array ( [timestamp] => 2017-02-17 01:21:13 [capture] => 1 [hint] => 1 ) ) ) [2] => Array ( [active] => 1 [type] => flag [title] => Example Level 3 [description] => This is the third example level. [entity_iso_code] => MA [category] => None [points] => 100 [bonus] => 30 [bonus_dec] => 10 [penalty] => 0 [teams] => Array ( [fbctf:user2:$2y$12$GIDv8cR7VpQ2OM0nzKVTTOGYgR4.pWTxOabtsPRH9JZ3yenKZx4m2] => Array ( [timestamp] => 2017-02-17 01:18:45 [capture] => 1 [hint] => ) [fbctf:user1:$2y$12$n.VmlNNwxmZ/OkGGuhVhFeXYzExAg0VoajyetLCIemSXN/yxWXLyO] => Array ( [timestamp] => 2017-02-17 01:01:41 [capture] => 1 [hint] => ) ) ) [3] => Array ( [active] => 1 [type] => flag [title] => Example Level 4 [description] => This is the second example level. [entity_iso_code] => RO [category] => None [points] => 100 [bonus] => 30 [bonus_dec] => 10 [penalty] => 0 [teams] => Array ( [fbctf:user3:$2y$12$GIDv8cR7V02OnzKVpQMTTOGYgR4.pWTsPOabtZRH9Jx3yenKZx4m2] => Array ( [timestamp] => 2017-02-17 01:09:24 [capture] => 1 [hint] => ) ) ) ) * TO DO (Enhancements): * Implemented alternative Live Sync key/authentication mechanisms, such as: Facebook Login, OAuth, etc. * Improve the processing of Bases/Progressive scoring. * Integrate password strength enforcement for the Live Sync credentials. * * Added unit tests for Live Sync to TeamTest * * Updated unit tests for the Live Sync API. * Added Google OAuth to Live Sync API * Google OAuth can now be used with the Live Sync when the exporting system provides the "google_oauth" type and provides the email address of the user in base64 encoded form. * Added Google OAuth UI/UX. If enabled, this allows a user to link their Google account to their FBCTF account using Google OAuth. The user simply navigates to the account page and clicks the "Link Your Google Account" button and completes the sign-in/authorization process. * Administrators must enable Google OAuth. When disabled the option does not appear for the users. To enable Google OAuth the administrator must first create a Google API account and then place the API secrets file on the system (in a non-web directory). The administrator would then set the full path to the API secrets file in the settings.ini file, within the GOOGLE_OAUTH_FILE field. * The Live Sync API has been updated to handle the "google_oauth" type case. * The liveimport.php script has been updated to set default values for some of the API fields. The following fields are mandatory: * title * description * points * teams * The live import code has also been updated to ensure duplicate levels, when using a combination of non-defined and defined countries, are not generated. * The project now requires google/apiclient ^2.0 from composer. Updated composer.json and composer.lock to define the new dependencies. * Minor formatting updates. * * Ensure mandatory fields are set, gracefully skip when they are not. * Refined Live Import CLI Options and Updated Google OAuth Data * The live sync import script (livesync.php) now utilizes `getopts()` to provide more user-friendly option input to the command-line script. The script will provide a help message upon usage without the required field(s). Here is the help message text: ``` Usage: hhvm -vRepo.Central.Path=/var/run/hhvm/.hhvm.hhbc_liveimport /var/www/fbctf/src/scripts/liveimport.php --url [Switched allowed multiple times. Optionally provide custom HTTP headers after URL, pipe delimited] --sleep
    +
    + + getValue()} + name="fb--conf--autorun_cycle" + /> +
    @@ -946,6 +962,43 @@ class="icon--badge" +
    +
    +

    {tr('LiveSync')}

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

    {tr('Internationalization')}

    @@ -1117,10 +1170,16 @@ public function renderControlsContent(): :xhp {
    +
    @@ -1135,23 +1194,6 @@ class="fb-cta cta--yellow" -
    -
    -
    - - -
    -
    -
    @@ -1179,6 +1221,32 @@ class="fb-cta cta--yellow" +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + +
    +
    +
    @@ -1277,6 +1345,41 @@ class="fb-cta cta--yellow" +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +

    {tr('Categories')}

    +
    +
    @@ -2969,8 +3072,8 @@ class={$highlighted_color} if (count($failures) > 0) { $failures_tbody = ; foreach ($failures as $failure) { - $level_genCheckStatus = await Level::genCheckStatus($failure->getLevelId()); - if (!$level_genCheckStatus) { + $check_status = await Level::genCheckStatus($failure->getLevelId()); + if (!$check_status) { continue; } $level = await Level::gen($failure->getLevelId()); diff --git a/src/controllers/Controller.php b/src/controllers/Controller.php index 84974f08..2c950ee7 100644 --- a/src/controllers/Controller.php +++ b/src/controllers/Controller.php @@ -43,8 +43,10 @@ abstract protected function genRenderBody(string $page): Awaitable<:xhp>; // TODO: Potential LFI - Review how to do internationalization better $document_root = must_have_string(Utils::getSERVER(), 'DOCUMENT_ROOT'); $language_style = ''; - if (file_exists($document_root. '/static/css/locals/' .$language. '/style.css')) { - $language_style = 'static/css/locals/' .$language. '/style.css'; + if (file_exists( + $document_root.'/static/css/locals/'.$language.'/style.css', + )) { + $language_style = 'static/css/locals/'.$language.'/style.css'; } return diff --git a/src/controllers/GameboardController.php b/src/controllers/GameboardController.php index 0d6cd0b1..bd84ad92 100644 --- a/src/controllers/GameboardController.php +++ b/src/controllers/GameboardController.php @@ -44,6 +44,9 @@ protected function getPages(): array {
  • {tr('Tutorial')}
  • +
  • + +
  • {$admin_link}
  • diff --git a/src/controllers/ajax/AdminAjaxController.php b/src/controllers/ajax/AdminAjaxController.php index 9f182190..832a6e27 100644 --- a/src/controllers/ajax/AdminAjaxController.php +++ b/src/controllers/ajax/AdminAjaxController.php @@ -129,17 +129,20 @@ protected function getActions(): array { 'pause_game', 'unpause_game', 'reset_game', + 'export_attachments', 'backup_db', 'export_game', 'export_teams', 'export_logos', 'export_levels', 'export_categories', + 'restore_db', 'import_game', 'import_teams', 'import_logos', 'import_levels', 'import_categories', + 'import_attachments', 'flush_memcached', 'reset_database', ); @@ -445,8 +448,11 @@ protected function getActions(): array { case 'unpause_game': await Control::genUnpause(); return Utils::ok_response('Success', 'admin'); + case 'export_attachments': + await Control::exportAttachments(); + return Utils::ok_response('Success', 'admin'); case 'backup_db': - Control::backupDb(); + await Control::backupDb(); return Utils::ok_response('Success', 'admin'); case 'export_game': await Control::exportGame(); @@ -463,6 +469,12 @@ protected function getActions(): array { case 'export_categories': await Control::exportCategories(); return Utils::ok_response('Success', 'admin'); + case 'restore_db': + $result = await Control::restoreDb(); + if ($result) { + return Utils::ok_response('Success', 'admin'); + } + return Utils::error_response('Error importing', 'admin'); case 'import_game': $result = await Control::importGame(); if ($result) { @@ -493,6 +505,12 @@ protected function getActions(): array { return Utils::ok_response('Success', 'admin'); } return Utils::error_response('Error importing', 'admin'); + case 'import_attachments': + $result = await Control::importAttachments(); + if ($result) { + return Utils::ok_response('Success', 'admin'); + } + return Utils::error_response('Error importing', 'admin'); case 'flush_memcached': $result = await Control::genFlushMemcached(); if ($result) { diff --git a/src/controllers/ajax/GameAjaxController.php b/src/controllers/ajax/GameAjaxController.php index ae102186..35c8e9ea 100644 --- a/src/controllers/ajax/GameAjaxController.php +++ b/src/controllers/ajax/GameAjaxController.php @@ -8,6 +8,8 @@ protected function getFilters(): array { 'level_id' => FILTER_VALIDATE_INT, 'answer' => FILTER_UNSAFE_RAW, 'csrf_token' => FILTER_UNSAFE_RAW, + 'livesync_username' => FILTER_UNSAFE_RAW, + 'livesync_password' => FILTER_UNSAFE_RAW, 'action' => array( 'filter' => FILTER_VALIDATE_REGEXP, 'options' => array('regexp' => '/^[\w-]+$/'), @@ -87,6 +89,18 @@ protected function getActions(): array { } case 'open_level': return Utils::ok_response('Success', 'admin'); + case 'set_livesync_password': + $livesync_password_update = await Team::genSetLiveSyncPassword( + SessionUtils::sessionTeam(), + "fbctf", + must_have_string($params, 'livesync_username'), + must_have_string($params, 'livesync_password'), + ); + if ($livesync_password_update === true) { + return Utils::ok_response('Success', 'game'); + } else { + return Utils::error_response('Failed', 'game'); + } default: return Utils::error_response('Invalid action', 'game'); } diff --git a/src/controllers/ajax/IndexAjaxController.php b/src/controllers/ajax/IndexAjaxController.php index e5e444eb..5e164e69 100644 --- a/src/controllers/ajax/IndexAjaxController.php +++ b/src/controllers/ajax/IndexAjaxController.php @@ -109,6 +109,8 @@ protected function getActions(): array { array $names, array $emails, ): Awaitable { + $ldap_password = $password; + // Check if registration is enabled $registration = await Configuration::gen('registration'); if ($registration->getValue() === '0') { @@ -199,6 +201,7 @@ protected function getActions(): array { // Verify that this team name is not created yet $team_exists = await Team::genTeamExist($shortname); if (!$team_exists) { + invariant(is_string($password), "Expected password to be a string"); $password_hash = Team::generateHash($password); $team_id = await Team::genCreate($shortname, $password_hash, $logo_name); @@ -216,7 +219,7 @@ protected function getActions(): array { } // Login the team if ($ldap->getValue() === '1') { - return await $this->genLoginTeam($team_id, $ldap_password); + return await $this->genLoginTeam($team_id, $ldap_password); } else { return await $this->genLoginTeam($team_id, $password); } diff --git a/src/controllers/importers/BinaryImporterController.php b/src/controllers/importers/BinaryImporterController.php new file mode 100644 index 00000000..9a87c300 --- /dev/null +++ b/src/controllers/importers/BinaryImporterController.php @@ -0,0 +1,12 @@ +contains($file_name)) { + $input_filename = $file[$file_name]['tmp_name']; + return $input_filename; + } + return false; + } +} diff --git a/src/controllers/modals/ActionModalController.php b/src/controllers/modals/ActionModalController.php index d2caac18..989eb9ab 100644 --- a/src/controllers/modals/ActionModalController.php +++ b/src/controllers/modals/ActionModalController.php @@ -189,14 +189,34 @@ class="fb-cta cta--yellow js-close-modal js-confirm-save"> ; return tuple($title, $content); + case 'restore-database': + $title = +

    + {tr('restore_')}{tr('Database')} +

    ; + $content = +
    +

    + {tr( + 'Are you sure you want to restore the database? This will overwrite ALL existing data!', + )} +

    + +
    ; + return tuple($title, $content); case 'reset-database': $title =

    @@ -219,6 +239,80 @@ class="fb-cta cta--yellow js-close-modal js-confirm-save">

  • ; return tuple($title, $content); + case 'account': + $title = +

    + {tr('account_')}{tr('Settings')} +

    ; + if (Configuration::genGoogleOAuthFileExists() === true) { + $google_oauth_content = + ; + } else { + $google_oauth_content = ''; + } + $content = +
    + {$google_oauth_content} +
    +

    + {tr( + 'Setup your FBCTF Live Sync credentials. These credentials must be the SAME on all other FBCTF instances that you are linking. DO NOT use your account password.', + )} +

    + + +
    ; + return tuple($title, $content); default: invariant(false, "Invalid modal name $modal"); } diff --git a/src/data/google_oauth.php b/src/data/google_oauth.php new file mode 100644 index 00000000..8a559e4f --- /dev/null +++ b/src/data/google_oauth.php @@ -0,0 +1,99 @@ +setAuthConfig($google_oauth_file); + $client->setAccessType('offline'); + $client->setScopes(['profile email']); + $client->setRedirectUri( + 'https://'.$_SERVER['HTTP_HOST'].'/data/google_oauth.php', + ); + + if (isset($_GET['code'])) { + $client->authenticate($_GET['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 (isset($_GET['error'])) { + $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/livesync.php b/src/data/livesync.php new file mode 100644 index 00000000..433fcef4 --- /dev/null +++ b/src/data/livesync.php @@ -0,0 +1,198 @@ + { + $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']; + + if ($livesync_enabled->getValue() === '1' && + hash_equals( + strval($livesync_auth_key->getValue()), + strval($input_auth_key), + )) { + + $livesync_enabled_awaits = Map { + 'all_teams' => Team::genAllTeams(), + 'all_scores' => ScoreLog::genAllScores(), + 'all_hints' => HintLog::genAllHints(), + 'all_levels' => Level::genAllLevels(), + }; + $livesync_enabled_awaits_results = + await \HH\Asio\m($livesync_enabled_awaits); + $all_teams = $livesync_enabled_awaits_results['all_teams']; + invariant( + is_array($all_teams), + 'all_teams should be an array and not null', + ); + + $all_scores = $livesync_enabled_awaits_results['all_scores']; + invariant( + is_array($all_scores), + 'all_scores should be an array and not null', + ); + + $all_hints = $livesync_enabled_awaits_results['all_hints']; + invariant( + is_array($all_hints), + 'all_hints should be an array and not null', + ); + + $all_levels = $livesync_enabled_awaits_results['all_levels']; + invariant( + is_array($all_levels), + 'all_levels should be an array and not null', + ); + + $data = array(); + $teams_array = array(); + $team_livesync_exists = Map {}; + $team_livesync_key = Map {}; + foreach ($all_teams as $team) { + $team_id = $team->getId(); + $team_livesync_exists->add( + Pair {$team_id, Team::genLiveSyncExists($team_id, "fbctf")}, + ); + } + $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")}, + ); + } + } + $team_livesync_key_results = await \HH\Asio\m($team_livesync_key); + $teams_array = $team_livesync_key_results->toArray(); + + $scores_array = array(); + $scored_teams = array(); + + foreach ($all_scores as $score) { + if (in_array($score->getTeamId(), array_keys($teams_array)) === + 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(); + } + foreach ($all_hints as $hint) { + if ($hint->getPenalty()) { + if (in_array($hint->getTeamId(), array_keys($teams_array)) === + 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(); + } + } + } + + $levels_array = array(); + $entities = Map {}; + $categories = Map {}; + foreach ($all_levels as $level) { + $level_id = $level->getId(); + $entities->add(Pair {$level_id, Country::gen($level->getEntityId())}); + $categories->add( + Pair { + $level_id, + Category::genSingleCategory($level->getCategoryId()), + }, + ); + } + $entities_results = await \HH\Asio\m($entities); + invariant( + $entities_results instanceof Map, + 'entities_results should of type Map and not null', + ); + + $categories_results = await \HH\Asio\m($categories); + invariant( + $categories_results instanceof Map, + 'categories_results should of type Map and not null', + ); + + foreach ($all_levels as $level) { + $level_id = $level->getId(); + $entity = $entities_results->get($level_id); + invariant( + $entity instanceof Country, + 'entity should of type Country and not null', + ); + + $category = $categories_results->get($level_id); + invariant( + $category instanceof Category, + 'category should of type Category and not null', + ); + + if (array_key_exists($level->getId(), $scores_array)) { + $score_level_array = $scores_array[$level_id]; + } else { + $score_level_array = array(); + } + $one_level = array( + 'active' => $level->getActive(), + 'type' => $level->getType(), + 'title' => $level->getTitle(), + 'description' => $level->getDescription(), + 'entity_iso_code' => $entity->getIsoCode(), + 'category' => $category->getCategory(), + 'points' => $level->getPoints(), + 'bonus' => $level->getBonusFix(), + 'bonus_dec' => $level->getBonusDec(), + 'penalty' => $level->getPenalty(), + 'teams' => $score_level_array, + ); + $levels_array[] = $one_level; + } + + $data = $levels_array; + } else if ($livesync_enabled->getValue() === '0') { + $data['error'] = tr( + 'LiveSync is disabled, please contact the administrator for access.', + ); + } else if (strval($input_auth_key) !== + strval($livesync_auth_key->getValue())) { + $data['error'] = + tr( + 'LiveSync auth key is invalid, please contact the administrator for access.', + ); + } else { + $data['error'] = tr( + 'LiveSync failed, please contact the administrator for assistance.', + ); + } + $this->jsonSend($data); + } + +} + +/* HH_IGNORE_ERROR[1002] */ +$syncData = new LiveSyncDataController(); +\HH\Asio\join($syncData->genGenerateData()); diff --git a/src/language/lang_hi.php b/src/language/lang_hi.php new file mode 100644 index 00000000..8cecda62 --- /dev/null +++ b/src/language/lang_hi.php @@ -0,0 +1,658 @@ + + 'H:i:s D d/m/Y', //used by date() function + //Translations for IndexController + 'Facebook CTF' => + ' Facebook CTF', + 'Conquer the world' => + 'दुनिया जीत लो', + 'Play' => + 'प्ले', + 'Welcome to the Facebook Capture the Flag Competition. By clicking "Play," you will be entered into the official CTF challenge. Good luck in your conquest.' => + 'Facebook पर आपका स्वागत है फ़लैग प्रतियोगिता को कैप्चर करें. "खेलें" क्लिक करके आप औपचारिक रूप से CTF चुनौती में शामिल हो जाएँगे. आपकी जीत के लिए शुभ कामनाएँ.', + 'Get ready for the CTF to start and access the board now!' => + 'अब CTF के शुरू होने और गेमबोर्ड एक्सेस करने के लिए तैयार रहें!!', + 'Gameboard' => + 'गेमबोर्ड', + 'Register Team' => + 'टीम पंजीकृत करें', + 'Get ready for the CTF to start and register your team now!' => + 'CTF के शुरू होने के लिए तैयार रहें और अपनी टीम को अभी पंजीकृत करें!', + 'Login' => + 'लॉग इन करें', + 'Soon' => + 'शीघ्र', + 'Upcoming Game' => + 'आगामी खेल', + '_days' => + '_दिन', + '_hours' => + '_घंटे', + '_minutes' => + '_मिनट', + '_seconds' => + '_सेकंड', + 'Official CTF Rules' => + 'आधिकारिक CTF नियम', + 'Following actions are prohibited, unless explicitly told otherwise by event Admins.' => + 'निम्न क्रियाएँ निषिद्ध हैं, जब तक कि ईवेंट व्यवस्थापक द्वारा स्पष्ट रूप से अन्यथा न बताया गया हो', + 'Rule' => + 'नियम', + 'Cooperation' => + 'सहयोग', + 'No cooperation between teams with independent accounts. Sharing of keys or providing revealing hints to other teams is cheating, don’t do it.' => + 'स्वतंत्र खातों वाली टीमों के बीच कोई सहयोग नहीं. चाबियों को साझा करना या अन्य टीमों को खुलासे के संकेत उपलब्ध कराना धोखा है, ऐसा न करें', + 'Attacking Scoreboard' => + 'आक्रमण स्कोरबोर्ड', + 'No attacking the competition infrastructure. If bugs or vulns are found, please alert the competition organizers immediately.' => + ' प्रतिस्पर्धी के बुनियादी ढांचे पर कोई आक्रमण नहीं. अगर बग या vulns पाए जाते हैं, तो कृपया प्रतियोगिता के आयोजकों को तुरंत सचेत करें.', + 'Sabotage' => + 'नुकसान ', + 'Absolutely no sabotaging of other competing teams, or in any way hindering their independent progress.' => + 'अन्य प्रतिस्पर्धी टीमों को नुकसान, या किसी भी तरह से उनकी स्वतंत्र प्रगति में बाधा बिलकुल उत्पन्न नहीं करना', + 'Bruteforcing' => + 'ब्रूटफ़ोर्सिंग', + 'No brute forcing of challenge flag/ keys against the scoring site.' => + 'स्कोर साइट के विरूद्ध चुनौती फ़्लैग/ कुंजियों की ब्रूटफ़ोर्सिंग नहीं', + 'Denial Of Service' => + 'सेवा की मनाही', + 'DoSing the CTF platform or any of the challenges is forbidden.' => + 'CTF प्लेटफ़ॉर्म या अन्य किसी भी चुनौती की डॉसिंग निषिद्ध है', + 'Legal' => + 'कानूनी', + 'Disclaimer' => + 'अस्वीकरण', + 'By participating in the contest, you agree to release Facebook and its employees, and the hosting organization from any and all liability, claims or actions of any kind whatsoever for injuries, damages or losses to persons and property which may be sustained in connection with the contest. You acknowledge and agree that Facebook et al is not responsible for technical, hardware or software failures, or other errors or problems which may occur in connection with the contest.' => + 'प्रतियोगिता में भाग लेने से, आप किसी भी और सभी दायित्व, दावे या चोट, क्षति या व्यक्तियों और संपत्ति को नुकसान के लिए किसी भी कार्यवाही, जो प्रतिस्पर्धा के संबंध में हो, के लिए Facebook और उसके कर्मचारियों, और होस्टिंग संगठनों को मुक्त करने के लिए सहमत होते हैं. आप स्वीकार करते हैं और मानते हैं कि Facebook और संबंधित सभी, प्रतिस्पर्धा के संबंध में होने वाली तकनीकी, हार्डवेयर या सॉफ्टवेयर विफलताओं, या अन्य त्रुटियों या समस्याओं के लिए जिम्मेदार नहीं है.', + 'If you have any questions about what is or is not allowed, please ask an organizer.' => + 'अगर आपके पास क्या अनुमत है और क्या नहीं, इस बारे में कोई सवाल हैं, तो किसी आयोजक से पूछें.', + 'Have fun!' => + 'मज़े करो!', + 'Name' => + 'नाम', + 'Email' => + 'ईमेल', + 'Token' => + 'टोकन', + 'Team Registration' => + 'टीम पंजीकरण', + 'Team Name' => + 'टीम का नाम', + 'Password' => + 'पासवर्ड', + 'Choose an Emblem' => + 'एक प्रतीक चुनें', + 'Sign Up' => + 'साइन अप करें', + 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => + 'फ़्लैग कैप्चर करें खेलने के लिए यहाँ रजिस्टर करें. एक बार जब आप पंजीकरण कर लेंगे, तो आपको लॉग इन किया जाएगा.', + 'Not Available' => + 'उपलब्ध नहीं है', + 'Team Registration will be open soon, stay tuned!' => + 'टीम पंजीकरण जल्द ही खोल दिया जाएगा, देखते रहें!', + 'Try Again' => + 'पुनः प्रयास करें', + 'Select' => + 'चुनें ', + 'Team Login' => + 'टीम लॉग इन', + 'Please login here. If you have not registered, you may do so by clicking "Sign Up" below. ' => + 'कृपया यहाँ लॉगिन करें. अगर आपने पंजीकरण नहीं किया है, तो आप नीचे "साइन अप करें" क्लिक करके ऐसा कर सकते हैं. ', + 'Team Login will be open soon, stay tuned!' => + 'टीम लॉग इन जल्द ही खोल दिया जाएगा, देखते रहें!', + 'ERROR' => + 'गूलती', + 'Start Over' => + 'फिर से शुरू करें', + 'Window is too small' => + 'विंडो बहुत छोटा है', + 'For the best CTF experience, please make window size bigger.' => + 'सर्वश्रेष्ठ CTF अनुभव के लिए, कृपया विंडो का आकार बड़ा करें.', + 'Thank you.' => + 'धन्यवाद. ', + 'Logout' => + 'लोग आउट', + 'Registration' => + 'पंजीकरण', + 'Play CTF' => + 'CTF खेलें', + 'Rules' => + 'नियम', + //Translations for GameboardController + 'Admin' => + 'व्यवस्थापक', + 'ADMIN' => + 'व्यवस्थापक', + 'Navigation' => + 'नेविगेशन', + 'View Mode' => + 'दृश्य मोड', + 'View mode' => + 'दृश्य मोड', + 'Tutorial' => + 'ट्यूटोरियल', + 'Scoreboard' => + 'स्कोरबोर्ड', + 'You' => + 'आप', + 'Others' => + 'अन्य', + 'All' => + 'सब', + 'Leaderboard' => + 'लीडरबोर्ड', + 'Announcements' => + 'घोषणाएँ', + 'Teams' => + ' टीम', + 'Filter' => + 'फिल्टर', + 'Activity' => + 'गतिविधि', + 'Game Clock' => + 'खेल घड़ी', + //Translations for AdminController + 'Auto' => + 'ऑटो', + 'All Categories' => + 'सभी श्रेणियाँ', + 'Open' => + 'खुला', + 'Tokenized' => + 'टोकन किया गया', + 'Hour' => + 'घंटा', + 'Hours' => + 'घंटे', + 'Used by' => + 'के द्वारा उपयोग', + 'Used By' => + 'के द्वारा उपयोग', + 'Available' => + 'उपलब्ध', + 'Registration Tokens' => + 'पंजीकरण टोकन', + 'Create More' => + 'अधिक बनाएँ', + 'Export Available' => + 'निर्यात उपलब्ध', + 'Not started yet' => + 'अभी तक शुरू नहीं हुआ', + 'Configuration' => + ' विन्यास', + 'Tokens' => + 'टोकन', + 'Game Configuration' => + 'खेल विन्यास', + 'OK' => + 'ठीक', + 'status_' => + 'स्थिति_', + 'On' => + 'चालू', + 'Off' => + 'बंद', + 'Player Names' => + 'खिलाड़ी के नाम', + 'Players Per Team' => + 'प्रति टीम खिलाड़ी', + 'Registration Type' => + 'पंजीकरण प्रकार', + 'Strong Passwords' => + 'मजबूत पासवर्ड', + 'Team Selection' => + 'टीम का चयन', + 'Game' => + 'खेल', + 'Scoring' => + 'स्कोरिंग', + 'Progressive Cycle (s)' => + 'प्रगतिशील चक्र (s)', + 'Refresh Gameboard' => + 'gameboard रीफ़्रेश करें', + 'Default Bonus' => + 'डिफ़ॉल्ट बोनस', + 'Bases Cycle (s)' => + 'बेस चक्र ', + 'Default Bonus Dec' => + ' डिफ़ॉल्ट बोनस दिसम्बर', + 'Timer' => + 'टाइमर', + 'Server Time' => + 'सर्वर समय', + 'Game Duration' => + 'खेल अवधि', + 'Begin Time' => + 'शुरू होने का समय', + 'Expected End Time' => + 'अपेक्षित समाप्ति समय', + 'Language' => + 'भाषा', + 'DELETE' => + 'हटाएँ', + 'Delete' => + 'हटाएँ', + 'No Announcements' => + 'कोई घोषणाएँ नहीं', + 'Game Controls' => + 'खेल नियंत्रण', + 'Write New Announcement here' => + 'नई घोषणा यहाँ लिखें', + 'Create' => + 'बनाएँ', + 'General' => + 'सामान्य', + 'Back Up Database' => + 'डेटाबेस बैक अप करें', + 'Export Full Game' => + 'पूरा खेल एक्सपोर्ट करें', + 'Import Full Game' => + 'पूरा खेल इम्पोर्ट करें', + 'Import Teams' => + 'टीम इम्पोर्ट करें', + 'Export Teams' => + 'टीम एक्सपोर्ट करें ', + 'Import Logos' => + 'लोगो इम्पोर्ट करें', + 'Export Logos' => + 'लोगो एक्सपोर्ट करें', + 'Import Levels' => + 'स्तर इम्पोर्ट करें', + 'Export Levels' => + 'स्तर एक्सपोर्ट करें', + 'Import Categories' => + 'श्रेणियाँ इम्पोर्ट करें', + 'Export Categories' => + 'श्रेणियाँ एक्सपोर्ट करें', + 'Levels' => + 'स्तर', + 'New Quiz Level' => + 'नया क्विज़ स्तर', + 'Title' => + 'शीर्षक', + 'Question' => + 'सवाल', + 'Level title' => + 'स्तर का शीर्षक', + 'Quiz question' => + 'क्विज़ सवाल', + 'Country' => + 'देश', + 'Answer' => + 'उत्तर', + 'Points' => + 'अंक', + 'Hint' => + 'सुझाव', + 'Hint Penalty' => + 'सुझाव जुर्माना', + 'EDIT' => + 'संपादन', + 'All Quiz Levels' => + 'सभी क्विज़ स्तर', + 'Filter By:' => + 'के द्वारा फ़िल्टर:', + 'All Status' => + 'सभी स्थिति', + 'Enabled' => + 'सक्षम', + 'Disabled' => + 'अक्षम', + 'Quiz Level' => + 'क्विज स्तर', + 'Show Answer' => + 'उत्तर दिखाओ', + 'Bonus' => + 'बोनस', + '-Dec' => + '-दिसं', + 'Save' => + 'सहेजें', + 'Quiz Management' => + 'क्विज प्रबंधन', + 'Add Quiz Level' => + 'क्विज़ स्तर जोड़ें', + 'New Flag Level' => + 'नया फ़्लैग स्तर', + 'Description' => + 'विवरण', + 'Level description' => + 'स्तर विवरण', + 'Category' => + 'श्रेणी', + 'Flag' => + 'फ़्लैग', + 'flag' => + 'फ़्लैग', + 'All Flag Levels' => + 'सभी फ़्लैग स्तर', + 'New Attachment:' => + 'नया अटैचमेंट:', + 'Attachment' => + ' अटैचमेंट', + 'Link' => + 'संपर्क', + 'New Link:' => + 'नई लिंक:', + 'Flag Level' => + 'फ़्लैग स्तर', + 'Categories' => + 'श्रेणियाँ', + '+ Attachment' => + '+ अटैचमेंट ', + '+ Link' => + '+ लिंक', + 'Flags Management' => + ' फ़्लैग प्रबंधन', + 'Add Flag Level' => + 'फ़्लैग स्तर जोड़ें', + 'New Base Level' => + 'नया बेस स्तर', + 'Keep Points' => + 'अंक रखें', + 'Capture points' => + 'अंक कैप्चर करें', + 'All Base Levels' => + 'सभी बेस स्तर', + 'Base Level' => + 'बेस स्तर', + 'Bases Management' => + 'बेस प्रबंधन', + 'Add Base Level' => + 'बेस स्तर जोड़ें', + 'New Category' => + 'नई श्रेणी', + 'Category: ' => + ' श्रेणी: ', + 'Categories Management' => + 'श्रेणी प्रबंधन', + 'Add Category' => + 'श्रेणी जोड़ें', + 'All Countries' => + 'सभी देश', + 'In Use' => + 'उपयोग में', + 'In use' => + 'उपयोग में', + 'Not Used' => + 'उपयोग नहीं किया', + 'Yes' => + 'हाँ', + 'No' => + 'नहीं', + 'ISO Code' => + 'आईएसओ कोड', + 'Countries Management' => + 'देश प्रबंधन', + 'No Team Names' => + 'कोई टीम नाम नहीं', + 'time' => + 'समय', + 'type' => + 'प्रकार', + 'pts' => + 'अंक', + 'Level' => + 'स्तर', + 'level' => + 'स्तर', + 'No Scores' => + 'कोई स्कोर नहीं', + 'Attempt' => + 'प्रयास', + 'No Failures' => + 'कोई विफलता नहीं', + 'Team' => + 'टीम', + 'team' => + 'टीम', + 'Names' => + 'नाम', + 'Scores' => + 'स्कोर', + 'Failures' => + 'विफलताएँ', + 'New Team' => + 'नई टीम', + 'Team Logo' => + 'टीम लोगो', + 'Selected Logo:' => + 'चुने लोगो:', + 'Select Logo' => + 'लोगो चुनें', + 'All Teams' => + 'सभी टीम', + 'Protected' => + 'संरक्षित', + 'Score' => + 'स्कोर', + 'Change Password' => + 'पासवर्ड बदलें', + 'Admin Level' => + 'व्यवस्थापक स्तर', + 'Visibility' => + 'दृश्यता', + 'Team Management' => + 'टीम प्रबंधन', + 'Add Team' => + 'टीम जोड़ें', + 'None' => + 'कोई नहीं', + 'Logo Name' => + 'लोगो का नाम', + 'Logo Management' => + 'लोगो प्रबंधन', + 'Session' => + 'सत्र', + 'Cookie' => + 'कुकी', + 'Creation Time' => + 'बनाने का समय', + 'Last Access' => + 'अंतिम एक्सेस', + 'Data' => + 'डेटा', + 'Sessions' => + 'सत्र', + 'entry' => + 'प्रविष्टि', + 'No Entries' => + 'कोई प्रविष्टि नहीं', + 'Game Logs' => + 'खेल लॉग', + 'Game Logs Timeline' => + 'खेल लॉग टाइमलाइन', + 'End Game' => + 'खेल समाप्त करें', + 'Begin Game' => + 'खेल शुरू करें', + 'Game Admin' => + 'खेल व्यवस्थापक', + 'Controls' => + 'नियंत्रण', + 'Quiz' => + 'क्विज', + 'Flags' => + 'फ़्लैग', + 'Bases' => + 'बेस', + 'Countries' => + 'देश', + 'Logos' => + 'लोगो', + //Translations for inc/* and inc/gameboard/* + 'captured' => + 'कैप्चर किया गया', + 'Status' => + 'स्थिति', + 'Completed' => + 'पूरा कर लिया है', + 'Remaining' => + 'शेष', + 'Start' => + 'प्रारंभ', + 'End' => + 'समाप्त', + 'Rank' => + 'रैंक', + 'pts' => + 'अंक', // अंक + 'Your Rank' => + 'आपका रैंक', + 'Your Score' => + 'आपका स्कोर', + 'Everyone' => + 'सभी', + 'Your Team' => + 'आपकी टीम', + 'Captured' => + 'कैप्चर किया गया', + 'Initiating' => + 'शुरू किया जा रहा है', + 'run : > boot_sequence' => + 'चलाएँ: > boot_sequence', + 'Extracting' => + 'एक्सट्रैक कर रहा है', + //Translations for Utils.php's time_ago() function + 'just now' => + 'अभी', + 'd' => + 'दि', //दिन + 'hr' => + 'घंटे', //घंटे + 'min' => + 'मिनट', //मिनट + 'sec' => + 'सेकंड', //सेकंड + 'ds' => + 'दि', //दिन + 'hrs' => + 'बजे', //घंटे + 'mins' => + 'मिनट', //मिनट + 'secs' => + 'सेकेंड', //सेकेंड + 'ago' => + 'पहले', + //Translations for ModalControllers + 'begin_' => + 'शुरू करें_', + 'Are you sure you want to kick off the game? Logs will be cleared and progressive scoreboard will start' => + 'क्या आप वाकई खेल बंद करना चाहते हैं? लॉग साफ हो जाएगा और प्रगतिशील स्कोरबोर्ड शुरू हो जाएगा', + 'end_' => + 'समाप्त_', + 'Are you sure you want to finish the current game?' => + 'क्या आप मौजूदा खेल खत्म करना चाहते हैं?', + 'Are you sure you want to logout from the game?' => + 'क्या आप वाकई इस खेल से लॉगआउट करना चाहते हैं?', + 'Saved' => + 'सहेजा गया', + 'All changes have been successfully saved.' => + 'सभी परिवर्तन सफलतापूर्वक सहेजे गए.', + 'Error' => + 'गलती', + 'Sorry your form was not saved. Please correct the all errors and save again.' => + 'माफ करें, आपका फार्म नहीं सहेजा गया. कृपया सभी गलतियों को ठीक करें और फिर से सहेजें.', + 'cancel_' => + 'रद्द करें_', + 'Are you sure you want to cancel? You have unsaved changes that will be reverted.' => + 'क्या आप वाकई रद्द करना चाहते हैं? आपके पास बिना सहेजे गए परिवर्तन हैं जो वापस कर दिए जाएँगे.', + 'choose_logo' => + 'Choose_logo', + 'captured_' => + 'कैप्चर किया गया_', + 'flag_owner_' => + 'flag_owner_', + 'INACTIVE' => + 'निष्क्रिय', + 'PTS' => + 'पीटीएस', + 'category' => + 'श्रेणी', + 'capture_' => + 'कैप्चर_', + 'Insert your answer' => + 'आपका जवाब डालें', + 'Request Hint' => + 'सुझाव का अनुरोध करें', + 'Submit' => + 'जमा करें', + 'hint_' => + 'Hint_', + 'first_capture' => + 'first_capture', + 'completed_by' => + 'द्वारा पूरा किया गया', + 'scoreboard_' => + 'scoreboard_', + 'filter_' => + 'filter_', + 'rank_' => + 'रैंक_', + 'team_name_' => + 'टीम का नाम_', + 'quiz_pts_' => + 'quiz_pts_', + 'flag_pts_' => + 'flag_pts_', + 'base_pts_' => + 'base_pts_', + 'total_pts_' => + 'total_pts_', + 'team_' => + 'team_', + 'team_members' => + 'टीम के सदस्य', + 'base_pts' => + 'base_pts', + 'quiz_pts' => + 'quiz_pts', + 'flag_pts' => + 'flag_pts', + 'total_pts' => + 'total_pts', + 'Tool bars are located on all edges of the gameboard. Tap a category to expand and close each tool bar.' => + 'टूल बार गेमबोर्ड के सभी किनारों पर स्थित हैं. हर टूल बार को विस्तृत और बंद करने के लिए किसी श्रेणी पर टैप करें.', + 'Tool_Bars' => + 'Tool_Bars', + 'Tap the "Game Clock" to keep track of time during gameplay. Don’t let time get the best of you.' => + 'गेमप्ले के दौरान समय का ट्रैक रखने के लिए "खेल घड़ी" टैप करें. इस समय के कारण आप अपना अच्छा प्रदर्शन न खोएँ.', + 'Game_Clock' => + 'Game_Clock', + 'Countries marked with an ' => + ' इसके साथ चिह्नित देश ', + 'are captured by you.' => + 'आप के द्वारा कैप्चर किए गए हैं.', + ' are owned by others.' => + 'दूसरों के स्वामित्व में हैं.', + 'Captures' => + 'कैप्चर', + 'Tap Plus[+] to Zoom In. Tap Minus[-] to Zoom Out.' => + 'ज़ूम इन करने के लिए प्लस [+] टैप करें. ज़ूम आउट करने के लिए माइनस[-] टैप करें.', + 'Click and Drag to move left, right, up and down.' => + 'बाएँ, दाएँ, ऊपर और नीचे ले जाने के लिए क्लिक करें और खींचें', + 'Zoom' => + 'ज़ूम', + 'Tap Forward Slash [/] to activate computer commands. A list of commands can be found under "Rules".' => + 'कंप्यूटर कमांड सक्रिय करने के लिए आगे का स्लेश [/] टैप करें. "नियम" के अंतर्गत कमांड की सूची प्राप्त की जा सकती है.', + 'Command_Line' => + 'कमांड लाइन', + 'Click "Nav" to access main navigation links like Rules of Play, Registration, Blog, Jobs & more.' => + ' खेल के नियम, पंजीकरण, ब्लॉग, नौकरी और अधिक जैसे मुख्य नेविगेशन एक्सेस करने के लिए "नेव" क्लिक करें.', + 'Track your competition by clicking "scorboard" to access real-time game statistics and graphs.' => + ' रीयल टाइम खेल सांख्यिकी और ग्राफ़ एक्सेस करने के लिए "स्कोरबोर्ड" क्लिक करके अपने प्रतिस्पर्धी का ट्रैक रखें.', + 'Have fun, be the best and conquer the world.' => + 'मज़े करें, सर्वश्रेष्ठ बनें और दुनिया जीतें', + 'Game_On' => + 'खेल शुरू', + 'tutorial_' => + 'Tutorial_', + 'Next' => + 'अगला', + 'Skip to play' => + 'खेलने के लिए स्किप करें ', + 'Powered By Facebook' => + ' Facebook द्वारा संचालित', +); diff --git a/src/models/Attachment.php b/src/models/Attachment.php index da43d3c5..6bdddcfd 100644 --- a/src/models/Attachment.php +++ b/src/models/Attachment.php @@ -17,6 +17,7 @@ private function __construct( private int $id, private int $levelId, private string $filename, + private string $type, ) {} public function getId(): int { @@ -27,6 +28,10 @@ public function getFilename(): string { return $this->filename; } + public function getType(): string { + return $this->type; + } + public function getLevelId(): int { return $this->levelId; } @@ -265,6 +270,22 @@ public function getLevelId(): int { } } + public static async function genImportAttachments( + int $level_id, + string $filename, + string $type, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'INSERT INTO attachments (filename, type, level_id, created_ts) VALUES (%s, %s, %d, NOW())', + $filename, + (string) $type, + $level_id, + ); + + return true; + } + private static function attachmentFromRow( Map $row, ): Attachment { @@ -272,6 +293,7 @@ private static function attachmentFromRow( intval(must_have_idx($row, 'id')), intval(must_have_idx($row, 'level_id')), must_have_idx($row, 'filename'), + must_have_idx($row, 'type'), ); } } diff --git a/src/models/Category.php b/src/models/Category.php index 67a0138f..f101cf6a 100644 --- a/src/models/Category.php +++ b/src/models/Category.php @@ -83,7 +83,8 @@ private static function categoryFromRow(Map $row): Category { if (!$mc_result || count($mc_result) === 0 || $refresh) { $db = await self::genDb(); $categories = array(); - $result = await $db->queryf('SELECT * FROM categories ORDER BY category ASC'); + $result = + await $db->queryf('SELECT * FROM categories ORDER BY category ASC'); foreach ($result->mapRows() as $row) { $categories[] = self::categoryFromRow($row); } diff --git a/src/models/Configuration.php b/src/models/Configuration.php index 2317bd34..dc344e40 100644 --- a/src/models/Configuration.php +++ b/src/models/Configuration.php @@ -131,4 +131,27 @@ private static function configurationFromRow( must_have_idx($row, 'description'), ); } + + public static function genGoogleOAuthFileExists(): bool { + $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 true; + } + 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']); + } + + return ''; + } } diff --git a/src/models/Control.php b/src/models/Control.php index 57c604fd..e6e259d3 100644 --- a/src/models/Control.php +++ b/src/models/Control.php @@ -216,6 +216,95 @@ class Control extends Model { await Level::genBaseScoring(); } + public static async function genAutoBegin(): Awaitable { + // 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()); + + // 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(); + } + } + + public static async function genAutoEnd(): Awaitable { + // 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()); + + // 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(); + } + } + + public static async function genAutoRun(): Awaitable { + // Get start time + $config_game = await Configuration::gen('game'); + $game = intval($config_game->getValue()); + + if ($game === 0) { + // Check and start the game + await Control::genAutoBegin(); + } else { + // Check and stop the game + await Control::genAutoEnd(); + } + } + + public static async function genRunAutoRunScript(): Awaitable { + $autorun_status = await Control::checkScriptRunning('autorun'); + if ($autorun_status === false) { + $autorun_location = escapeshellarg( + must_have_string(Utils::getSERVER(), 'DOCUMENT_ROOT'). + '/scripts/autorun.php', + ); + $cmd = + 'hhvm -vRepo.Central.Path=/var/run/hhvm/.hhvm.hhbc_autorun '. + $autorun_location. + ' > /dev/null 2>&1 & echo $!'; + $pid = shell_exec($cmd); + await Control::genStartScriptLog(intval($pid), 'autorun', $cmd); + } + } + + public static async function checkScriptRunning( + string $name, + ): Awaitable { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT pid FROM scripts WHERE name = %s AND status = 1 LIMIT 1', + $name, + ); + 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(); + } + return $status; + } else { + return false; + } + } + public static async function importGame(): Awaitable { $data_game = JSONImporterController::readJSON('game_file'); if (is_array($data_game)) { @@ -251,6 +340,7 @@ class Control extends Model { if (!$levels_result) { return false; } + await self::genFlushMemcached(); return true; } return false; @@ -260,6 +350,7 @@ class Control extends Model { $data_teams = JSONImporterController::readJSON('teams_file'); if (is_array($data_teams)) { $teams = must_have_idx($data_teams, 'teams'); + await self::genFlushMemcached(); return await Team::importAll($teams); } return false; @@ -269,6 +360,7 @@ class Control extends Model { $data_logos = JSONImporterController::readJSON('logos_file'); if (is_array($data_logos)) { $logos = must_have_idx($data_logos, 'logos'); + await self::genFlushMemcached(); return await Logo::importAll($logos); } return false; @@ -278,6 +370,7 @@ class Control extends Model { $data_levels = JSONImporterController::readJSON('levels_file'); if (is_array($data_levels)) { $levels = must_have_idx($data_levels, 'levels'); + await self::genFlushMemcached(); return await Level::importAll($levels); } return false; @@ -287,11 +380,55 @@ class Control extends Model { $data_categories = JSONImporterController::readJSON('categories_file'); if (is_array($data_categories)) { $categories = must_have_idx($data_categories, 'categories'); + await self::genFlushMemcached(); return await Category::importAll($categories); } return false; } + public static async function importAttachments(): Awaitable { + $output = array(); + $status = 0; + $filename = + strval(BinaryImporterController::getFilename('attachments_file')); + $document_root = must_have_string(Utils::getSERVER(), 'DOCUMENT_ROOT'); + $directory = $document_root.Attachment::attachmentsDir; + $cmd = "tar -zx -C $directory -f $filename"; + exec($cmd, $output, $status); + if (intval($status) !== 0) { + return false; + } + $directory_files = scandir($directory); + foreach ($directory_files as $file) { + $chmod = chmod($directory.$file, 0600); + invariant( + $chmod === true, + 'Failed to set attachment file permissions to 0600', + ); + } + await self::genFlushMemcached(); + return true; + } + + public static async function restoreDb(): Awaitable { + $output = array(); + $status = 0; + $filename = + strval(BinaryImporterController::getFilename('database_file')); + $cmd = "cat $filename | gunzip - "; + exec($cmd, $output, $status); + if (intval($status) !== 0) { + return false; + } + $cmd = "cat $filename | gunzip - | ".Db::getInstance()->getRestoreCmd(); + exec($cmd, $output, $status); + if (intval($status) !== 0) { + return false; + } + await self::genFlushMemcached(); + return true; + } + public static async function exportGame(): Awaitable { $game = array(); $logos = await Logo::exportAll(); @@ -335,12 +472,24 @@ class Control extends Model { exit(); } - public static function backupDb(): void { + public static async function exportAttachments(): Awaitable { + $filename = 'fbctf-attachments-'.date("d-m-Y").'.tgz'; + header('Content-Type: application/x-tgz'); + header('Content-Disposition: attachment; filename="'.$filename.'"'); + $document_root = must_have_string(Utils::getSERVER(), 'DOCUMENT_ROOT'); + $directory = $document_root.Attachment::attachmentsDir; + $cmd = "tar -cz -C $directory . "; + passthru($cmd); + exit(); + } + + public static async function backupDb(): Awaitable { $filename = 'fbctf-backup-'.date("d-m-Y").'.sql.gz'; header('Content-Type: application/x-gzip'); header('Content-Disposition: attachment; filename="'.$filename.'"'); $cmd = Db::getInstance()->getBackupCmd().' | gzip --best'; passthru($cmd); + exit(); } public static async function genAllActivity( diff --git a/src/models/Level.php b/src/models/Level.php index 994e023e..67176dba 100644 --- a/src/models/Level.php +++ b/src/models/Level.php @@ -158,7 +158,7 @@ private static function levelFromRow(Map $row): Level { if (!$exist && $entity_exist && $category_exist) { $entity = await Country::genCountry($entity_iso_code); $category = await Category::genSingleCategoryByName($c); - await self::genCreate( + $level_id = await self::genCreate( $type, $title, must_have_string($level, 'description'), @@ -172,6 +172,23 @@ private static function levelFromRow(Map $row): Level { must_have_string($level, 'hint'), must_have_int($level, 'penalty'), ); + $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); + } + $attachments = must_have_idx($level, 'attachments'); + invariant( + is_array($attachments), + 'attachments must be of type array', + ); + foreach ($attachments as $attachment) { + await Attachment::genImportAttachments( + $level_id, + $attachment['filename'], + $attachment['type'], + ); + } } } return true; @@ -186,6 +203,19 @@ private static function levelFromRow(Map $row): Level { foreach ($all_levels as $level) { $entity = await Country::gen($level->getEntityId()); $category = await Category::genSingleCategory($level->getCategoryId()); + $links = await Link::genAllLinks($level->getId()); + $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[] = [ + 'filename' => $attachment->getFilename(), + 'type' => $attachment->getType(), + ]; + } $one_level = array( 'type' => $level->getType(), 'title' => $level->getTitle(), @@ -200,6 +230,8 @@ private static function levelFromRow(Map $row): Level { 'flag' => $level->getFlag(), 'hint' => $level->getHint(), 'penalty' => $level->getPenalty(), + 'links' => $link_array, + 'attachments' => $attachment_array, ); array_push($all_levels_data, $one_level); } @@ -1175,4 +1207,87 @@ public static function getBasesResponses( return false; } } + + public static async function getLevelIdByTypeTitleCountry( + string $type, + string $title, + string $entity_iso_code, + ): Awaitable { + $db = await self::genDb(); + + $result = + await $db->queryf( + 'SELECT id FROM levels WHERE type = %s AND title = %s AND entity_id IN (SELECT id FROM countries WHERE iso_code = %s)', + $type, + $title, + $entity_iso_code, + ); + + invariant($result->numRows() === 1, 'Expected exactly one result'); + return intval(must_have_idx($result->mapRows()[0], 'id')); + } + + public static async function genAlreadyExistUnknownCountry( + string $type, + string $title, + string $description, + int $points, + ): Awaitable { + $db = await self::genDb(); + + $result = + await $db->queryf( + 'SELECT COUNT(*) FROM levels WHERE type = %s AND title = %s AND description = %s AND points = %d', + $type, + $title, + $description, + $points, + ); + + if ($result->numRows() > 0) { + invariant($result->numRows() === 1, 'Expected exactly one result'); + return (intval(idx($result->mapRows()[0], 'COUNT(*)')) > 0); + } else { + return false; + } + } + + public static async function genLevelIdUnknownCountry( + string $type, + string $title, + string $description, + int $points, + ): Awaitable { + $db = await self::genDb(); + + $result = + await $db->queryf( + 'SELECT id FROM levels WHERE type = %s AND title = %s AND description = %s AND points = %d', + $type, + $title, + $description, + $points, + ); + + invariant($result->numRows() === 1, 'Expected exactly one result'); + return intval(must_have_idx($result->mapRows()[0], 'id')); + } + + public static async function genLevelUnknownCountry( + string $type, + string $title, + string $description, + int $points, + ): Awaitable { + + $level_id = await self::genLevelIdUnknownCountry( + $type, + $title, + $description, + $points, + ); + + $level = await self::gen($level_id); + return $level; + } } diff --git a/src/models/ScoreLog.php b/src/models/ScoreLog.php index 7c1cc7f4..2a68699f 100644 --- a/src/models/ScoreLog.php +++ b/src/models/ScoreLog.php @@ -225,4 +225,83 @@ private static function scorelogFromRow(Map $row): ScoreLog { MultiTeam::invalidateMCRecords('TEAMS_BY_LEVEL'); // Invalidate Memcached MultiTeam data. MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. } + + public static async function genScoreLogUpdate( + int $level_id, + int $team_id, + int $points, + string $type, + string $timestamp, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'UPDATE scores_log SET ts = %s, level_id = %d, team_id = %d, points = %d, type = %s WHERE level_id = %d AND team_id = %d', + $timestamp, + $level_id, + $team_id, + $points, + $type, + $level_id, + $team_id, + ); + self::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. + Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control 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. + } + + public static async function genUpdateScoreLogBonus( + int $level_id, + int $team_id, + int $points, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'UPDATE scores_log SET ts = ts, points = %d WHERE level_id = %d AND team_id = %d', + $points, + $level_id, + $team_id, + ); + self::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. + Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control 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. + } + + public static async function genLevelScores( + int $level_id, + ): Awaitable> { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT * FROM scores_log WHERE level_id = %d ORDER BY ts ASC', + $level_id, + ); + + $scores = array(); + foreach ($result->mapRows() as $row) { + $scores[] = self::scorelogFromRow($row); + } + + return $scores; + } + + public static async function genLevelScoreByTeam( + int $team_id, + int $level_id, + ): Awaitable { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT * FROM scores_log WHERE team_id = %d AND level_id = %d', + $team_id, + $level_id, + ); + + return self::scorelogFromRow($result->mapRows()[0]); + } } diff --git a/src/models/Session.php b/src/models/Session.php index fd86ccd7..4ca6b62c 100644 --- a/src/models/Session.php +++ b/src/models/Session.php @@ -111,7 +111,7 @@ private static function sessionFromRow(Map $row): Session { ): Awaitable { $db = await self::genDb(); await $db->queryf( - 'INSERT INTO sessions (cookie, data, created_ts, last_access_ts, team_id, last_page_access) VALUES (%s, %s, NOW(), NOW(), 1, %s)', + 'INSERT INTO sessions (cookie, data, created_ts, last_access_ts, team_id, last_page_access) VALUES (%s, %s, NOW(), NOW(), 0, %s)', $cookie, $data, Router::getRequestedPage(), diff --git a/src/models/Team.php b/src/models/Team.php index bdf1ad74..696ad92d 100644 --- a/src/models/Team.php +++ b/src/models/Team.php @@ -665,4 +665,253 @@ public static function regenerateHash(string $password_hash): bool { return $rank; } + + public static async function genTeamUpdatePoints( + int $team_id, + int $points, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'UPDATE teams SET last_score = last_score, points = %d WHERE id = %d', + $points, + $team_id, + ); + MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. + Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control data. + } + + public static async function genGetLiveSyncKey( + int $team_id, + string $type, + ): Awaitable { + $db = await self::genDb(); + $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; + // FALLTHROUGH + default: + $key = $key_from_db; + break; + } + + return strval($type.":".$username.":".$key); + } + + public static async function genLiveSyncExists( + int $team_id, + string $type, + ): Awaitable { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT id FROM livesync WHERE team_id = %d AND type = %s', + $team_id, + $type, + ); + if ($result->numRows() === 1) { + return true; + } + return false; + } + + public static async function genSetLiveSyncPassword( + int $team_id, + string $type, + string $username, + string $password, + ): Awaitable { + $db = await self::genDb(); + + if (($username === '') || ($password === '')) { + return false; + } + + switch ($type) { + case 'fbctf': + $key = hash("sha256", $password); + $team = await self::genTeam($team_id); + if (password_verify($password, $team->getPasswordHash())) { + return false; + } + break; + // FALLTHROUGH + default: + $key = $password; + break; + } + + $username_result = + await $db->queryf( + 'SELECT id FROM livesync WHERE username = %s AND type = %s AND team_id != %d', + $username, + $type, + $team_id, + ); + if ($username_result->numRows() > 0) { + return false; + } + + $current_id_result = await $db->queryf( + 'SELECT id FROM livesync WHERE team_id = %d AND type = %s', + $team_id, + $type, + ); + if ($current_id_result->numRows() === 1) { + $result = await $db->queryf( + 'UPDATE livesync SET username = %s, sync_key = %s WHERE id = %d', + $username, + $key, + intval(must_have_idx($current_id_result->mapRows()[0], 'id')), + ); + if ($result) { + return true; + } + } else { + $result = + await $db->queryf( + 'INSERT INTO livesync (type, team_id, username, sync_key) VALUES (%s, %d, %s, %s)', + $type, + $team_id, + $username, + $key, + ); + if ($result) { + return true; + } + } + return false; + } + + public static async function genLiveSyncKeyExists( + string $key, + ): Awaitable { + $db = await self::genDb(); + + if (strpos($key, ':') === false) { + return false; + } + list($type, $username, $key) = explode(':', $key); + + switch ($type) { + case 'fbctf': + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE username = %s AND type = %s', + $username, + $type, + ); + break; + case 'google_oauth': + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE sync_key = %s AND type = %s', + $key, + $type, + ); + break; + // FALLTHROUGH + default: + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE sync_key = %s', + $key, + ); + break; + } + + if ($result->numRows() > 0) { + $team_id = 0; + foreach ($result->mapRows() as $row) { + $type = strval(must_have_idx($row, 'type')); + $username = strval(must_have_idx($row, 'username')); + $key_from_db = strval(must_have_idx($row, 'sync_key')); + + switch ($type) { + case 'fbctf': + if (password_verify($key_from_db, $key)) { + return true; + } + break; + // FALLTHROUGH + default: + if (strval($key) === strval($key_from_db)) { + return true; + } + break; + } + } + } + return false; + } + + public static async function genTeamFromLiveSyncKey( + string $key, + ): Awaitable { + $db = await self::genDb(); + + invariant(strpos($key, ':'), "Invalid live sync key"); + list($type, $username, $key) = explode(':', $key); + + switch ($type) { + case 'fbctf': + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE username = %s AND type = %s', + $username, + $type, + ); + invariant($result->numRows() > 0, 'Expected at least one result'); + break; + case 'google_oauth': + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE sync_key = %s AND type = %s', + $key, + $type, + ); + break; + // FALLTHROUGH + default: + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE sync_key = %s', + $key, + ); + invariant($result->numRows() > 0, 'Expected at least one result'); + break; + } + + $team_id = 0; + foreach ($result->mapRows() as $row) { + $type = strval(must_have_idx($row, 'type')); + $username = strval(must_have_idx($row, 'username')); + $key_from_db = strval(must_have_idx($row, 'sync_key')); + + switch ($type) { + case 'fbctf': + if (password_verify($key_from_db, $key)) { + $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)) { + $team_id = intval(must_have_idx($row, 'team_id')); + $team = await self::genTeam($team_id); + return $team; + } + break; + } + } + invariant($team_id !== 0, 'team_id not found'); + $team = await self::genTeam($team_id); + return $team; + } + } diff --git a/src/scripts/autorun.php b/src/scripts/autorun.php new file mode 100644 index 00000000..9f45bffc --- /dev/null +++ b/src/scripts/autorun.php @@ -0,0 +1,48 @@ +getValue()); + $sleep = $conf_sleep_secs; + $conf_game = \HH\Asio\join(Configuration::gen('game')); + $config_start_ts = \HH\Asio\join(Configuration::gen('start_ts')); + $start_ts = intval($config_start_ts->getValue()); + $config_end_ts = \HH\Asio\join(Configuration::gen('end_ts')); + $end_ts = intval($config_end_ts->getValue()); + + if (($conf_game->getValue() === '1') && + (($end_ts - time()) < $conf_sleep_secs)) { + $sleep = $end_ts - time(); + } else if (($conf_game->getValue() === '0') && + (($start_ts - time()) < $conf_sleep_secs)) { + $sleep = $start_ts - time(); + } + + if ($sleep < 0) { + $sleep = $conf_sleep_secs; + } + + sleep($sleep); +} diff --git a/src/scripts/liveimport.php b/src/scripts/liveimport.php new file mode 100644 index 00000000..c5ea2c50 --- /dev/null +++ b/src/scripts/liveimport.php @@ -0,0 +1,595 @@ + [Switched allowed multiple times. Optionally provide custom HTTP headers after URL, pipe delimited] \n". + " --sleep
    +
    + +
    + + + + +
    +
    diff --git a/src/inc/gameboard/modules/activity.php b/src/inc/gameboard/modules/activity.php index 88a1824d..c98bc5fd 100644 --- a/src/inc/gameboard/modules/activity.php +++ b/src/inc/gameboard/modules/activity.php @@ -11,26 +11,55 @@ class ActivityModuleController { await tr_start(); $activity_ul =
      ; - $all_activity = await Control::genAllActivity(); + $all_activity = await ActivityLog::genAllActivity(); $config = await Configuration::gen('language'); $language = $config->getValue(); - foreach ($all_activity as $score) { - if (intval($score['team_id']) === SessionUtils::sessionTeam()) { - $class_li = 'your-team'; - $class_span = 'your-name'; + foreach ($all_activity as $activity) { + $subject = $activity->getSubject(); + $entity = $activity->getEntity(); + $ts = $activity->getTs(); + if (($subject !== '') && ($entity !== '')) { + $class_li = ''; + $class_span = ''; + list($subject_type, $subject_id) = + explode(':', $activity->getSubject()); + list($entity_type, $entity_id) = explode(':', $activity->getEntity()); + if ($subject_type === 'Team') { + if (intval($subject_id) === SessionUtils::sessionTeam()) { + $class_li = 'your-team'; + $class_span = 'your-name'; + } else { + $class_li = 'opponent-team'; + $class_span = 'opponent-name'; + } + } + if ($entity_type === 'Country') { + $formatted_entity = locale_get_display_region( + '-'.$activity->getFormattedEntity(), + $language, + ); + } else { + $formatted_entity = $activity->getFormattedEntity(); + } + $activity_ul->appendChild( +
    • + [ {time_ago($ts)} ] + + {$activity->getFormattedSubject()} +  {tr($activity->getAction())}  + {$formatted_entity} +
    • + ); } else { - $class_li = 'opponent-team'; - $class_span = 'opponent-name'; + $activity_ul->appendChild( +
    • + [ {time_ago($ts)} ] + + {$activity->getFormattedMessage()} + +
    • + ); } - $translated_country = - locale_get_display_region('-'.$score['country'], $language); - $activity_ul->appendChild( -
    • - [ {time_ago($score['time'])} ] - {$score['team']}  - {tr('captured')} {$translated_country} -
    • - ); } return diff --git a/src/models/ActivityLog.php b/src/models/ActivityLog.php new file mode 100644 index 00000000..9f860cb2 --- /dev/null +++ b/src/models/ActivityLog.php @@ -0,0 +1,277 @@ + + $MC_KEYS = Map {'ALL_ACTIVITY' => 'activity'}; + + private function __construct( + private int $id, + private string $subject, + private string $action, + private string $entity, + private string $message, + private string $arguments, + private string $ts, + private string $formatted_subject = '', + private string $formatted_entity = '', + private string $formatted_message = '', + ) { + $formatted_subject = + \HH\Asio\join(self::genFormatString("%s", $this->subject)); + $this->formatted_subject = $formatted_subject; + $formatted_entity = + \HH\Asio\join(self::genFormatString("%s", $this->entity)); + $this->formatted_entity = $formatted_entity; + $formatted_message = + \HH\Asio\join(self::genFormatString($this->message, $this->arguments)); + $this->formatted_message = $formatted_message; + } + + public function getId(): int { + return $this->id; + } + + public function getSubject(): string { + return $this->subject; + } + + public function getAction(): string { + return $this->action; + } + + public function getEntity(): string { + return $this->entity; + } + + public function getMessage(): string { + return $this->message; + } + + public function getArguments(): string { + return $this->arguments; + } + + public function getFormattedSubject(): string { + return $this->formatted_subject; + } + + public function getFormattedEntity(): string { + return $this->formatted_entity; + } + + public function getFormattedMessage(): string { + return $this->formatted_message; + } + + public function getTs(): string { + return $this->ts; + } + + private static function activitylogFromRow( + Map $row, + ): ActivityLog { + return new ActivityLog( + intval(must_have_idx($row, 'id')), + must_have_idx($row, 'subject'), + must_have_idx($row, 'action'), + must_have_idx($row, 'entity'), + must_have_idx($row, 'message'), + must_have_idx($row, 'arguments'), + must_have_idx($row, 'ts'), + ); + } + + public static async function genFormatString( + string $string, + string $arguments, + ): Awaitable { + if ($arguments !== '') { + $variables = array(); + $values_array = explode(',', $arguments); + foreach ($values_array as $value) { + list($class, $id) = explode(':', $value); + switch ($class) { + case "Team": + $team_exists = await Team::genTeamExistById(intval($id)); + if ($team_exists === true) { + $team = await Team::genTeam(intval($id)); + $variables[] = $team->getName(); + } else { + return ''; + } + break; + case "Level": + $level_exists = await Level::genAlreadyExistById(intval($id)); + if ($level_exists === true) { + $level = await Level::gen(intval($id)); + $variables[] = $level->getTitle(); + } else { + return ''; + } + break; + case "Country": + $country_exists = await Country::genCheckExistsById(intval($id)); + if ($country_exists === true) { + $country = await Country::gen(intval($id)); + $variables[] = $country->getIsoCode(); + } else { + return ''; + } + break; + // FALLTHROUGH + default: + return ''; + break; + } + } + $formatted = vsprintf($string, $variables); + return $formatted; + } + return $string; + } + + public static async function genCreate( + string $subject, + string $action, + string $entity, + string $message, + string $arguments, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'INSERT INTO activity_log (ts, subject, action, entity, message, arguments) (SELECT NOW(), %s, %s, %s, %s, %s) LIMIT 1', + $subject, + $action, + $entity, + $message, + $arguments, + ); + + self::invalidateMCRecords(); // Invalidate Memcached ActivityLog data. + } + + public static async function genCaptureLog( + int $team_id, + int $level_id, + ): Awaitable { + //$level = await Level::gen($level_id); + $country_id = await Level::genCountryIdForLevel($level_id); + await self::genCreateActionLog( + "Team", + $team_id, + "captured", + "Country", + $country_id, + ); + } + + public static async function genCreateActionLog( + string $subject_class, + int $subject_id, + string $action, + string $entity_class, + int $entity_id, + ): Awaitable { + await self::genCreate( + "$subject_class:$subject_id", + $action, + "$entity_class:$entity_id", + '', + '', + ); + } + + public static async function genCreateGameActionLog( + string $subject_class, + int $subject_id, + string $action, + string $entity_class, + int $entity_id, + ): Awaitable { + $config_game = await Configuration::gen('game'); + $config_pause = await Configuration::gen('game_paused'); + if ((intval($config_game->getValue()) === 1) && + (intval($config_pause->getValue()) === 0)) { + await self::genCreate( + "$subject_class:$subject_id", + $action, + "$entity_class:$entity_id", + '', + '', + ); + } + } + + public static async function genAdminLog( + string $action, + string $entity_class, + int $entity_id, + ): Awaitable { + if (SessionUtils::sessionActive() === false) { + return; + } + await self::genCreateGameActionLog( + "Team", + SessionUtils::sessionTeam(), + $action, + $entity_class, + $entity_id, + ); + } + + public static async function genCreateGenericLog( + string $message, + string $arguments = '', + ): Awaitable { + await self::genCreate('', '', '', $message, $arguments); + } + + public static async function genDelete( + int $activity_log_id, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'DELETE FROM activity_log WHERE id = %d LIMIT 1', + $activity_log_id, + ); + + self::invalidateMCRecords(); // Invalidate Memcached Announcement data. + } + + public static async function genDeleteAll(): Awaitable { + $db = await self::genDb(); + await $db->queryf('TRUNCATE TABLE activity_log'); + + self::invalidateMCRecords(); // Invalidate Memcached Announcement data. + } + + public static async function genAllActivity( + bool $refresh = false, + ): Awaitable> { + $mc_result = self::getMCRecords('ALL_ACTIVITY'); + 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'); + foreach ($result->mapRows() as $row) { + $activity_log = self::activitylogFromRow($row); + if (($activity_log->getFormattedMessage() !== '') || + (($activity_log->getFormattedSubject() !== '') && + ($activity_log->getFormattedEntity() !== ''))) { + $activity_log_lines[] = $activity_log; + } + } + self::setMCRecords('ALL_ACTIVITY', $activity_log_lines); + return $activity_log_lines; + } + invariant( + is_array($mc_result), + 'cache return should be an array of ActivityLog', + ); + return $mc_result; + } +} diff --git a/src/models/Announcement.php b/src/models/Announcement.php index 957cdaa5..cd2b87ca 100644 --- a/src/models/Announcement.php +++ b/src/models/Announcement.php @@ -47,6 +47,25 @@ private static function announcementFromRow( self::invalidateMCRecords(); // Invalidate Memcached Announcement data. } + public static async function genCreateAuto( + string $announcement, + ): Awaitable { + $config_game = await Configuration::gen('game'); + $config_pause = await Configuration::gen('game_paused'); + if ((intval($config_game->getValue()) === 1) && + (intval($config_pause->getValue()) === 0)) { + $auto_announce = await Configuration::gen('auto_announce'); + if ($auto_announce->getValue() === '1') { + $db = await self::genDb(); + await $db->queryf( + 'INSERT INTO announcements_log (ts, announcement) (SELECT NOW(), %s) LIMIT 1', + $announcement, + ); + self::invalidateMCRecords(); // Invalidate Memcached Announcement data. + } + } + } + public static async function genDelete( int $announcement_id, ): Awaitable { @@ -59,6 +78,13 @@ private static function announcementFromRow( self::invalidateMCRecords(); // Invalidate Memcached Announcement data. } + public static async function genDeleteAll(): Awaitable { + $db = await self::genDb(); + await $db->queryf('TRUNCATE TABLE announcements_log'); + + self::invalidateMCRecords(); // Invalidate Memcached Announcement data. + } + // Get all tokens. public static async function genAllAnnouncements( bool $refresh = false, diff --git a/src/models/Control.php b/src/models/Control.php index e6e259d3..4e166582 100644 --- a/src/models/Control.php +++ b/src/models/Control.php @@ -47,6 +47,18 @@ class Control extends Model { // 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(); @@ -122,6 +134,12 @@ class Control extends Model { } 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'); @@ -155,6 +173,12 @@ 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'); @@ -214,6 +238,12 @@ class Control extends Model { // 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!'); } public static async function genAutoBegin(): Awaitable { diff --git a/src/models/Country.php b/src/models/Country.php index f20e7a11..748584b4 100644 --- a/src/models/Country.php +++ b/src/models/Country.php @@ -330,4 +330,23 @@ private static function countryFromRow(Map $row): Country { } } + // Check if a country already exists, by id + public static async function genCheckExistsById( + int $entity_id, + ): Awaitable { + $db = await self::genDb(); + + $result = await $db->queryf( + 'SELECT COUNT(*) FROM countries WHERE id = %d', + $entity_id, + ); + + if ($result->numRows() > 0) { + invariant($result->numRows() === 1, 'Expected exactly one result'); + return (intval(idx($result->mapRows()[0], 'COUNT(*)')) > 0); + } else { + return false; + } + } + } diff --git a/src/models/Level.php b/src/models/Level.php index 67176dba..3d3182bc 100644 --- a/src/models/Level.php +++ b/src/models/Level.php @@ -379,6 +379,15 @@ private static function levelFromRow(Map $row): Level { self::invalidateMCRecords(); // Invalidate Memcached Level data. 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. + return intval(must_have_idx($result->mapRows()[0], 'id')); } @@ -592,29 +601,36 @@ private static function levelFromRow(Map $row): Level { $ent_id = $entity_id; } - await $db->queryf( - 'UPDATE levels SET title = %s, description = %s, entity_id = %d, category_id = %d, points = %d, '. - 'bonus = %d, bonus_dec = %d, bonus_fix = %d, flag = %s, hint = %s, '. - 'penalty = %d WHERE id = %d LIMIT 1', - $title, - $description, - $ent_id, - $category_id, - $points, - $bonus, - $bonus_dec, - $bonus_fix, - $flag, - $hint, - $penalty, - $level_id, - ); + $result = + await $db->queryf( + 'UPDATE levels SET title = %s, description = %s, entity_id = %d, category_id = %d, points = %d, '. + 'bonus = %d, bonus_dec = %d, bonus_fix = %d, flag = %s, hint = %s, '. + 'penalty = %d WHERE id = %d LIMIT 1', + $title, + $description, + $ent_id, + $category_id, + $points, + $bonus, + $bonus_dec, + $bonus_fix, + $flag, + $hint, + $penalty, + $level_id, + ); // Make sure entities are consistent await Country::genUsedAdjust(); - self::invalidateMCRecords(); // Invalidate Memcached Level data. - Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control data. + 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!"); + self::invalidateMCRecords(); // Invalidate Memcached Level data. + ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. + } } // Delete level. @@ -637,13 +653,21 @@ private static function levelFromRow(Map $row): Level { ): Awaitable { $db = await self::genDb(); - await $db->queryf( + $result = await $db->queryf( 'UPDATE levels SET active = %d WHERE id = %d LIMIT 1', (int) $active, $level_id, ); - self::invalidateMCRecords(); // Invalidate Memcached Level data. + 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. + } } // Enable or disable levels by type. @@ -653,13 +677,15 @@ private static function levelFromRow(Map $row): Level { ): Awaitable { $db = await self::genDb(); - await $db->queryf( + $results = await $db->queryf( 'UPDATE levels SET active = %d WHERE type = %s', (int) $active, $type, ); - self::invalidateMCRecords(); // Invalidate Memcached Level data. + if ($results->numRowsAffected() > 0) { + self::invalidateMCRecords(); // Invalidate Memcached Level data. + } } // Enable or disable all levels. @@ -670,19 +696,20 @@ private static function levelFromRow(Map $row): Level { $db = await self::genDb(); if ($type === 'all') { - await $db->queryf( - 'UPDATE levels SET active = %d WHERE id > 0', - (int) $active, + $result = await $db->queryf( + 'SELECT id FROM levels WHERE active = %d AND id >0', + (int) !$active, ); } else { - await $db->queryf( - 'UPDATE levels SET active = %d WHERE type = %s', - (int) $active, + $result = await $db->queryf( + 'SELECT id FROM levels WHERE active = %d AND type = %s', + (int) !$active, $type, ); } - - self::invalidateMCRecords(); // Invalidate Memcached Level data. + foreach ($result->mapRows() as $row) { + await self::genSetStatus(intval($row->get('id')), $active); + } } // All levels. @@ -939,7 +966,7 @@ private static function levelFromRow(Map $row): Level { $lock = fopen($lock_name, 'w'); if ($lock === false) { - error_log('Failed to open lock file $lock_name'); + error_log('Failed to open lock file '.$lock_name); return null; } if (!flock($lock, LOCK_EX)) { @@ -1094,7 +1121,7 @@ private static function levelFromRow(Map $row): Level { // Log the hint await HintLog::genLogGetHint($level_id, $team_id, $penalty); - Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control 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. @@ -1208,6 +1235,33 @@ public static function getBasesResponses( } } + // Check if a level already exists by type, title and entity. + public static async function genAlreadyExistById( + int $level_id, + ): Awaitable { + $db = await self::genDb(); + + $result = await $db->queryf( + 'SELECT COUNT(*) FROM levels WHERE id = %d', + $level_id, + ); + + if ($result->numRows() > 0) { + invariant($result->numRows() === 1, 'Expected exactly one result'); + return (intval(idx($result->mapRows()[0], 'COUNT(*)')) > 0); + } else { + return false; + } + } + + // Check if a level already exists by type, title and entity. + public static async function genCountryIdForLevel( + int $level_id, + ): Awaitable { + $level = await self::gen($level_id); + return $level->getEntityId(); + } + public static async function getLevelIdByTypeTitleCountry( string $type, string $title, @@ -1234,7 +1288,6 @@ public static function getBasesResponses( int $points, ): Awaitable { $db = await self::genDb(); - $result = await $db->queryf( 'SELECT COUNT(*) FROM levels WHERE type = %s AND title = %s AND description = %s AND points = %d', @@ -1243,7 +1296,6 @@ public static function getBasesResponses( $description, $points, ); - if ($result->numRows() > 0) { invariant($result->numRows() === 1, 'Expected exactly one result'); return (intval(idx($result->mapRows()[0], 'COUNT(*)')) > 0); diff --git a/src/models/ScoreLog.php b/src/models/ScoreLog.php index 2a68699f..c611e40c 100644 --- a/src/models/ScoreLog.php +++ b/src/models/ScoreLog.php @@ -69,7 +69,7 @@ private static function scorelogFromRow(Map $row): ScoreLog { $db = await self::genDb(); await $db->queryf('DELETE FROM scores_log WHERE id > 0'); self::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. - Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control 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. @@ -217,8 +217,9 @@ private static function scorelogFromRow(Map $row): ScoreLog { $points, $type, ); + await ActivityLog::genCaptureLog($team_id, $level_id); self::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. - Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control 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. diff --git a/src/models/Team.php b/src/models/Team.php index 696ad92d..d2289eed 100644 --- a/src/models/Team.php +++ b/src/models/Team.php @@ -345,7 +345,7 @@ public static function regenerateHash(string $password_hash): bool { $team_id, ); MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. - Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control data. + ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. } // Update team password. @@ -381,7 +381,7 @@ public static function regenerateHash(string $password_hash): bool { $team_id, ); MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. - Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control data. + ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. await Session::genDeleteByTeam($team_id); } @@ -442,7 +442,7 @@ public static function regenerateHash(string $password_hash): bool { $team_id, ); MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. - Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control data. + ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. } // Check if a team name is already created. @@ -464,6 +464,23 @@ public static function regenerateHash(string $password_hash): bool { } } + // Check if a team name is already created. + public static async function genTeamExistById( + int $team_id, + ): Awaitable { + $db = await self::genDb(); + + $result = + await $db->queryf('SELECT COUNT(*) FROM teams WHERE id = %d', $team_id); + + if ($result->numRows() > 0) { + invariant($result->numRows() === 1, 'Expected exactly one result'); + return (intval(idx($result->mapRows()[0], 'COUNT(*)')) > 0); + } else { + return false; + } + } + // All active teams. public static async function genAllActiveTeams(): Awaitable> { $db = await self::genDb(); @@ -608,7 +625,7 @@ public static function regenerateHash(string $password_hash): bool { $db = await self::genDb(); await $db->queryf('UPDATE teams SET points = 0 WHERE id > 0'); MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. - Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control data. + ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. } // Teams total number. diff --git a/tests/_files/seed.xml b/tests/_files/seed.xml index 1abec738..b3d08f6a 100644 --- a/tests/_files/seed.xml +++ b/tests/_files/seed.xml @@ -135,6 +135,24 @@ en description + + 3 + game + + description + + + 4 + auto_announce + 0 + description + + + 5 + game_paused + 0 + description + id diff --git a/tests/models/ConfigurationTest.php b/tests/models/ConfigurationTest.php index e20064d9..9df8c735 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(2, count($all)); + $this->assertEquals(5, count($all)); $c = $all[0]; $this->assertEquals(1, $c->getId()); diff --git a/tests/models/LevelTest.php b/tests/models/LevelTest.php index 5ae2769b..e9bc7c0a 100644 --- a/tests/models/LevelTest.php +++ b/tests/models/LevelTest.php @@ -127,10 +127,10 @@ public function testDelete(): void { } public function testSetStatus(): void { - HH\Asio\join(Level::genSetStatus(1, false)); + HH\Asio\join(Level::genSetStatus(2, false)); $all = HH\Asio\join(Level::genAllLevels()); $this->assertEquals(3, count($all)); - $l = $all[0]; + $l = $all[1]; $this->assertFalse($l->getActive()); } From dc7c87c3cc5e8ea899ed207fc3f4f1d5bb2b411c Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 6 Jun 2017 16:01:10 -0400 Subject: [PATCH 06/18] Level Import Fix (#514) * Added backward compatibility to the Level import process for exported files prior to PR #451 * Levels exported after PR #451 contain Links and Attachments, however prior to PR #451 they did not. The import code will now import levels even if the import file doesn't contain Links or Attachments. --- src/models/Level.php | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/models/Level.php b/src/models/Level.php index 3d3182bc..50f66278 100644 --- a/src/models/Level.php +++ b/src/models/Level.php @@ -172,22 +172,26 @@ private static function levelFromRow(Map $row): Level { must_have_string($level, 'hint'), must_have_int($level, 'penalty'), ); - $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); + if (array_key_exists('links', $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); + } } - $attachments = must_have_idx($level, 'attachments'); - invariant( - is_array($attachments), - 'attachments must be of type array', - ); - foreach ($attachments as $attachment) { - await Attachment::genImportAttachments( - $level_id, - $attachment['filename'], - $attachment['type'], + if (array_key_exists('attachments', $level)) { + $attachments = must_have_idx($level, 'attachments'); + invariant( + is_array($attachments), + 'attachments must be of type array', ); + foreach ($attachments as $attachment) { + await Attachment::genImportAttachments( + $level_id, + $attachment['filename'], + $attachment['type'], + ); + } } } } From c5da9f78e3ded3bcfee8d6252f2cecdda2cdd941 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 6 Jun 2017 16:01:35 -0400 Subject: [PATCH 07/18] Announcements Controls Rename (#515) * Renamed "Game Controls" to "Announcement Controls" on the administrative Announcement page. --- src/controllers/AdminController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index f589554f..87de7cb9 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -1139,7 +1139,7 @@ class="icon--badge" return
      -

      {tr('Game Controls')}

      +

      {tr('Announcement Controls')}

      {tr('status_')}{tr('OK')} From ec996a511d35d9244f2f85b62524c688242578a8 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 6 Jun 2017 16:02:08 -0400 Subject: [PATCH 08/18] Set Default Scoring Cache Values (#516) * Before a game begins, or before any scores are captured, the Memcached is empty for multiple scoring values. This results in continual hits to the database. * Scores are now cached at zero until the first capture is obtained. This dramatically reduces the number of queries performed and the load on the server. --- src/models/MultiTeam.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/models/MultiTeam.php b/src/models/MultiTeam.php index d8cf1876..7c9c8929 100644 --- a/src/models/MultiTeam.php +++ b/src/models/MultiTeam.php @@ -130,6 +130,12 @@ class MultiTeam extends Team { ); $points_by_type->add(Pair {intval($team->get('id')), $type_pair}); } + } else { + $type_pair = Map {}; + $type_pair->add(Pair {'quiz', 0}); + $type_pair->add(Pair {'flag', 0}); + $type_pair->add(Pair {'base', 0}); + $points_by_type->add(Pair {intval($team->get('id')), $type_pair}); } } self::setMCRecords('POINTS_BY_TYPE', new Map($points_by_type)); From 6d4f9196174c41a8a88fb3d526b9ac8cd4471fe0 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 6 Jun 2017 16:03:09 -0400 Subject: [PATCH 09/18] Unique Logos Per Team # (#517) * Functionality prevents users from utilizing the same logo as another user/team or one already in use. * Users now are provided the option of selecting a unique, or unused logo. * When a logo is selected the logo is marked as used. * When a logo is removed from a team (through team deletion, logo change, or otherwise) the logo is readded to the rotation for available logos. * When a team is imported their logo is set to used. * Database schema changed to set the default for all logos to unused. The scheme update also sets the admin logo to used. --- database/logos.sql | 4 ++-- src/models/Logo.php | 16 +++++++++++++++- src/models/Team.php | 23 +++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/database/logos.sql b/database/logos.sql index cf03ed1a..245a7df4 100644 --- a/database/logos.sql +++ b/database/logos.sql @@ -7,7 +7,7 @@ DROP TABLE IF EXISTS `logos`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `logos` ( `id` int(11) NOT NULL AUTO_INCREMENT, - `used` tinyint(1) DEFAULT 1, + `used` tinyint(1) DEFAULT 0, `enabled` tinyint(1) DEFAULT 1, `protected` tinyint(1) DEFAULT 0, `custom` tinyint(1) DEFAULT 0, @@ -23,7 +23,7 @@ CREATE TABLE `logos` ( LOCK TABLES `logos` WRITE; /*!40000 ALTER TABLE `logos` DISABLE KEYS */; -INSERT INTO `logos` (name, logo, protected, custom) VALUES ('admin', '/static/svg/icons/badges/badge-admin.svg', 1, 0); +INSERT INTO `logos` (name, logo, protected, used, custom) VALUES ('admin', '/static/svg/icons/badges/badge-admin.svg', 1, 1, 0); INSERT INTO `logos` (name, logo, custom) VALUES ('4chan-2', '/static/svg/icons/badges/badge-4chan-2.svg', 0); INSERT INTO `logos` (name, logo, custom) VALUES ('4chan', '/static/svg/icons/badges/badge-4chan.svg', 0); INSERT INTO `logos` (name, logo, custom) VALUES ('8ball', '/static/svg/icons/badges/badge-8ball.svg', 0); diff --git a/src/models/Logo.php b/src/models/Logo.php index a4f69ec5..0d28726a 100644 --- a/src/models/Logo.php +++ b/src/models/Logo.php @@ -93,6 +93,20 @@ public function getCustom(): bool { self::invalidateMCRecords(); } + // Set logo as used or unused by passing 1 or 0. + public static async function genSetUsed( + string $logo_name, + bool $used, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'UPDATE logos SET used = %d WHERE name = %s LIMIT 1', + (int) $used, + $logo_name, + ); + self::invalidateMCRecords(); + } + // Retrieve a random logo from the table. public static async function genRandomLogo(): Awaitable { $all_logos = await self::genAllLogos(); @@ -148,7 +162,7 @@ public function getCustom(): bool { $all_enabled_logos = array(); $result = await $db->queryf( - 'SELECT * FROM logos WHERE enabled = 1 AND protected = 0 AND custom = 0', + 'SELECT * FROM logos WHERE enabled = 1 AND used = 0 AND protected = 0 AND custom = 0', ); foreach ($result->mapRows() as $row) { diff --git a/src/models/Team.php b/src/models/Team.php index d2289eed..edebcf60 100644 --- a/src/models/Team.php +++ b/src/models/Team.php @@ -99,6 +99,7 @@ protected static function teamFromRow(Map $row): Team { (bool) must_have_idx($team, 'visible'), ); } + await Logo::genSetUsed(must_have_string($team, 'logo'), true); } return true; } @@ -247,6 +248,8 @@ public static function regenerateHash(string $password_hash): bool { $logo, ); + await Logo::genSetUsed($logo, true); + // Return newly created team_id $result = await $db->queryf( @@ -256,6 +259,7 @@ public static function regenerateHash(string $password_hash): bool { $logo, ); + Logo::invalidateMCRecords(); MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. invariant($result->numRows() === 1, 'Expected exactly one result'); return intval($result->mapRows()[0]['id']); @@ -286,6 +290,7 @@ public static function regenerateHash(string $password_hash): bool { $protected ? 1 : 0, $visible ? 1 : 0, ); + await Logo::genSetUsed($logo, true); // Return newly created team_id $result = @@ -296,6 +301,7 @@ public static function regenerateHash(string $password_hash): bool { $logo, ); + Logo::invalidateMCRecords(); MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. invariant($result->numRows() === 1, 'Expected exactly one result'); return intval($result->mapRows()[0]['id']); @@ -337,6 +343,14 @@ public static function regenerateHash(string $password_hash): bool { int $team_id, ): Awaitable { $db = await self::genDb(); + + // Get and set old logo to unused + $result = + 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, @@ -344,6 +358,9 @@ public static function regenerateHash(string $password_hash): bool { $points, $team_id, ); + await Logo::genSetUsed($logo, true); + + Logo::invalidateMCRecords(); MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. ActivityLog::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached ActivityLog data. } @@ -366,6 +383,12 @@ public static function regenerateHash(string $password_hash): bool { // Delete team. public static async function genDelete(int $team_id): Awaitable { $db = await self::genDb(); + $result = + 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, From ea78f6adc5f8cad95ebc7eae23cdc29a9cd21af1 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 6 Jun 2017 16:05:24 -0400 Subject: [PATCH 10/18] Custom Branding Update (#518) * Added "Custom Organization," editable within the administrative interface. The organization will display as the system name (i.e., "Custom CTF") in all relevant locations including the page titles. * Custom Text has been renamed to "Custom Byline." * The custom organization value will also be used for the welcome message on the landing page. * Database updated to support the new customization options. --- database/schema.sql | 3 ++- database/test_schema.sql | 4 ++++ src/controllers/AdminController.php | 25 +++++++++++++++++++------ src/controllers/Controller.php | 8 ++++---- src/controllers/GameboardController.php | 3 ++- src/controllers/IndexController.php | 15 ++++++++++----- src/controllers/ViewModeController.php | 3 ++- 7 files changed, 43 insertions(+), 18 deletions(-) diff --git a/database/schema.sql b/database/schema.sql index e89565ac..82a2966b 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -233,7 +233,8 @@ INSERT INTO `configuration` (field, value, description) VALUES("language", "en", INSERT INTO `configuration` (field, value, description) VALUES("livesync", "0", "(Boolean) LiveSync functionality"); INSERT INTO `configuration` (field, value, description) VALUES("livesync_auth_key", "", "(String) Optional LiveSync Auth Key"); INSERT INTO `configuration` (field, value, description) VALUES("custom_logo", "0", "(Boolean) Custom branding logo"); -INSERT INTO `configuration` (field, value, description) VALUES("custom_text", "Powered By Facebook", "(String) Custom branding text"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_org", "Facebook", "(String) Custom branding organization text"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_byline", "Powered By Facebook", "(String) Custom branding byline text"); INSERT INTO `configuration` (field, value, description) VALUES("custom_logo_image", "static/img/favicon.png", "(String) Custom logo image file"); UNLOCK TABLES; diff --git a/database/test_schema.sql b/database/test_schema.sql index cf6b5ef9..5beadecb 100644 --- a/database/test_schema.sql +++ b/database/test_schema.sql @@ -232,6 +232,10 @@ INSERT INTO `configuration` (field, value, description) VALUES("default_bonusdec INSERT INTO `configuration` (field, value, description) VALUES("language", "en", "(String) Language of the system"); INSERT INTO `configuration` (field, value, description) VALUES("livesync", "0", "(Boolean) LiveSync functionality"); INSERT INTO `configuration` (field, value, description) VALUES("livesync_auth_key", "", "(String) Optional LiveSync Auth Key"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_logo", "0", "(Boolean) Custom branding logo"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_org", "Facebook", "(String) Custom branding organization text"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_byline", "Powered By Facebook", "(String) Custom branding byline text"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_logo_image", "static/img/favicon.png", "(String) Custom logo image file"); UNLOCK TABLES; -- diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index 87de7cb9..7a226213 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -3,7 +3,8 @@ class AdminController extends Controller { <<__Override>> protected function getTitle(): string { - return tr('Facebook CTF').' | '.tr('Admin'); + $custom_org = \HH\Asio\join(Configuration::gen('custom_org')); + return tr($custom_org->getValue()). ' '. tr('CTF'). ' | '. tr('Admin'); } <<__Override>> @@ -316,7 +317,8 @@ class="fb-cta cta--yellow" 'livesync' => Configuration::gen('livesync'), 'livesync_auth_key' => Configuration::gen('livesync_auth_key'), 'custom_logo' => Configuration::gen('custom_logo'), - 'custom_text' => Configuration::gen('custom_text'), + 'custom_org' => Configuration::gen('custom_org'), + 'custom_byline' => Configuration::gen('custom_byline'), 'custom_logo_image' => Configuration::gen('custom_logo_image'), }; @@ -347,7 +349,8 @@ class="fb-cta cta--yellow" $livesync = $results['livesync']; $livesync_auth_key = $results['livesync_auth_key']; $custom_logo = $results['custom_logo']; - $custom_text = $results['custom_text']; + $custom_org = $results['custom_org']; + $custom_byline = $results['custom_byline']; $custom_logo_image = $results['custom_logo_image']; $registration_on = $registration->getValue() === '1'; @@ -1074,11 +1077,21 @@ class="icon--badge"
      - + getValue()} + name="fb--conf--custom_org" + value={$custom_org->getValue()} + /> +
      +
      +
      +
      + + getValue()} />
      diff --git a/src/controllers/Controller.php b/src/controllers/Controller.php index 2c950ee7..a29368f5 100644 --- a/src/controllers/Controller.php +++ b/src/controllers/Controller.php @@ -10,22 +10,22 @@ abstract protected function genRenderBody(string $page): Awaitable<:xhp>; public async function genRenderBranding(): Awaitable<:xhp> { $awaitables = Map { 'custom_logo' => Configuration::gen('custom_logo'), - 'custom_text' => Configuration::gen('custom_text'), + 'custom_byline' => Configuration::gen('custom_byline'), 'custom_logo_image' => Configuration::gen('custom_logo_image'), }; $results = await \HH\Asio\m($awaitables); $branding = $results['custom_logo']; - $custom_text = $results['custom_text']; + $custom_byline = $results['custom_byline']; if ($branding->getValue() === '0') { $branding_xhp = getValue()))} + brandingText={tr(strval($custom_byline->getValue()))} />; } else { $custom_logo_image = $results['custom_logo_image']; $branding_xhp = getValue())} + brandingText={strval($custom_byline->getValue())} brandingLogo={strval($custom_logo_image->getValue())} />; } diff --git a/src/controllers/GameboardController.php b/src/controllers/GameboardController.php index bd84ad92..83920d01 100644 --- a/src/controllers/GameboardController.php +++ b/src/controllers/GameboardController.php @@ -3,7 +3,8 @@ class GameboardController extends Controller { <<__Override>> protected function getTitle(): string { - return tr('Facebook CTF').' | '.tr('Gameboard'); + $custom_org = \HH\Asio\join(Configuration::gen('custom_org')); + return tr($custom_org->getValue()). ' '. tr('CTF').' | '.tr('Gameboard'); } <<__Override>> diff --git a/src/controllers/IndexController.php b/src/controllers/IndexController.php index d7f1920f..af6991f9 100644 --- a/src/controllers/IndexController.php +++ b/src/controllers/IndexController.php @@ -2,8 +2,9 @@ class IndexController extends Controller { <<__Override>> - protected function getTitle(): string { - return tr('Facebook CTF'); + public function getTitle(): string { + $custom_org = \HH\Asio\join(Configuration::gen('custom_org')); + return tr($custom_org->getValue()). ' '. tr('CTF'); } <<__Override>> @@ -38,6 +39,12 @@ protected function getPages(): array { } public function renderMainContent(): :xhp { + $custom_org = \HH\Asio\join(Configuration::gen('custom_org')); + if ($custom_org->getValue() === 'Facebook') { + $welcome_msg = tr('Welcome to the Facebook Capture the Flag Competition. By clicking "Play," you will be entered into the official CTF challenge. Good luck in your conquest.'); + } else { + $welcome_msg = 'Welcome to the ' . $custom_org->getValue() . ' Capture the Flag Competition. By clicking "Play," you will be entered into the official CTF challenge. Good luck in your conquest.'; + } return

      - {tr( - 'Welcome to the Facebook Capture the Flag Competition. By clicking "Play," you will be entered into the official CTF challenge. Good luck in your conquest.', - )} + {$welcome_msg}

      diff --git a/src/controllers/ViewModeController.php b/src/controllers/ViewModeController.php index 172e92ac..d70d1926 100644 --- a/src/controllers/ViewModeController.php +++ b/src/controllers/ViewModeController.php @@ -3,7 +3,8 @@ class ViewModeController extends Controller { <<__Override>> protected function getTitle(): string { - return tr('Facebook CTF').' | '.tr('View mode'); + $custom_org = \HH\Asio\join(Configuration::gen('custom_org')); + return tr($custom_org->getValue()).' | '.tr('View mode'); } <<__Override>> From eb4a5b5e0e4eef73a8abecfaa1fb182613af147c Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 6 Jun 2017 16:06:09 -0400 Subject: [PATCH 11/18] Backup and Restore settings.ini on Tests (#519) * If `settings.ini` exists it will now be copied to `settings.ini.bak` during the testing process (/extra/run_tests.sh). * After the tests complete, if the backup file exists it will restore the file. --- extra/run_tests.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/extra/run_tests.sh b/extra/run_tests.sh index 0144aee7..719680a9 100755 --- a/extra/run_tests.sh +++ b/extra/run_tests.sh @@ -20,6 +20,11 @@ mysql -u "$DB_USER" --password="$DB_PWD" "$DB" -e "source $CODE_PATH/database/te mysql -u "$DB_USER" --password="$DB_PWD" "$DB" -e "source $CODE_PATH/database/logos.sql;" mysql -u "$DB_USER" --password="$DB_PWD" "$DB" -e "source $CODE_PATH/database/countries.sql;" +if [ -f "$CODE_PATH/settings.ini" ]; then + echo "[+] Backing up existing settings.ini" + cp "$CODE_PATH/settings.ini" "$CODE_PATH/settings.ini.bak" +fi + echo "[+] DB Connection file" cat "$CODE_PATH/extra/settings.ini.example" | sed "s/DATABASE/$DB/g" | sed "s/MYUSER/$DB_USER/g" | sed "s/MYPWD/$DB_PWD/g" > "$CODE_PATH/settings.ini" @@ -30,6 +35,11 @@ echo "[+] Deleting test database" mysql -u "$DB_USER" --password="$DB_PWD" -e "DROP DATABASE IF EXISTS $DB;" mysql -u "$DB_USER" --password="$DB_PWD" -e "FLUSH PRIVILEGES;" +if [ -f "$CODE_PATH/settings.ini.bak" ]; then + echo "[+] Restoring previous settings.ini" + mv "$CODE_PATH/settings.ini.bak" "$CODE_PATH/settings.ini" +fi + # In the future, we should use the hh_client exit status. # Current there are some PHP built-ins not found in the hhi files upstream in HHVM. echo "[+] Verifying HHVM Strict Compliance and Error Checking" From 5d91ae92a8fb0336ef6fdba7404996b72d80fb7f Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 6 Jun 2017 16:06:49 -0400 Subject: [PATCH 12/18] Maintain Team Protection on Database Reset (#520) * Maintain Team Protection on Database Reset * Database resets, through Admin Control, will maintain protection on protected admin accounts. * Added Team::genSetProtected() to set protection status on a team. * Database reset now calls Team::genSetProtected() on previous protected accounts. * The Database reset process has been optimized to improve performance. * Fixed Permissions --- src/models/Control.php | 10 +++++----- src/models/Team.php | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/models/Control.php b/src/models/Control.php index 4e166582..25d00b8c 100644 --- a/src/models/Control.php +++ b/src/models/Control.php @@ -572,15 +572,15 @@ class Control extends Model { $logos = await self::genLoadDatabaseFile('../database/logos.sql'); if ($schema && $countries && $logos) { foreach ($admins as $admin) { - await Team::genCreate( + $team_id = await Team::genCreate( $admin->getName(), $admin->getPasswordHash(), $admin->getLogo(), ); - } - $teams = await MultiTeam::genAllTeamsCache(); - foreach ($teams as $team) { - await Team::genSetAdmin($team->getId(), true); + await Team::genSetAdmin($team_id, true); + if ($admin->getProtected() === true) { + await Team::genSetProtected($team_id, true); + } } await self::genFlushMemcached(); return true; diff --git a/src/models/Team.php b/src/models/Team.php index edebcf60..2946a8ae 100644 --- a/src/models/Team.php +++ b/src/models/Team.php @@ -438,6 +438,20 @@ public static function regenerateHash(string $password_hash): bool { MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. } + // Sets toggles team protection status. + public static async function genSetProtected( + int $team_id, + bool $protect, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'UPDATE teams SET protected = %d WHERE id = %d LIMIT 1', + $protect ? 1 : 0, + $team_id, + ); + MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. + } + // Sets toggles team admin status. public static async function genSetAdmin( int $team_id, From 2b1474bb39ac31e8737ba79fd441820110fd6ced Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 6 Jun 2017 16:07:38 -0400 Subject: [PATCH 13/18] Fixed Login Form JS Bug (Fixes: #521) (#523) * Login form JavaScript now properly retrieves and renders errors when invalid login credentials are provided. * Added teamLoginFormError() distinguished from teamNameFormError() and teamTokenFormError(). * Updated teamPasswordFormError() to read the login AJAX response and call the appropriate error function. * This PR fixes the bug identified in Issue #521. --- src/static/js/index.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/static/js/index.js b/src/static/js/index.js index 289fa87f..7a6f4263 100644 --- a/src/static/js/index.js +++ b/src/static/js/index.js @@ -7,6 +7,13 @@ function teamNameFormError() { }); } +function teamLoginFormError() { + $('.el--text')[0].classList.add('form-error'); + $('.fb-form input').on('change', function() { + $('.el--text')[0].classList.remove('form-error'); + }); +} + function teamPasswordFormError(toosimple) { $('.el--text')[1].classList.add('form-error'); if (toosimple) { @@ -115,11 +122,16 @@ function sendIndexRequest(request_data) { goToPage(responseData.redirect); } else { // TODO: Make this a modal - verifyTeamName('register'); if (responseData.message === 'Password too simple') { teamPasswordFormError(true); } - teamTokenFormError(); + if (responseData.message === 'Login failed') { + teamLoginFormError(); + } + if (responseData.message === 'Registration failed') { + teamNameFormError(); + teamTokenFormError(); + } } }); } From 4a7b5b526d0ac4cc6291bbaf517e556477f65b42 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Wed, 21 Jun 2017 13:26:54 -0400 Subject: [PATCH 14/18] Level Deletion Confirmation and Bug Fixes (#512) * Level Deletion Confirmation and Bug Fixes * Added deletion confirmation dialog when deleting levels. * Level data is now deleted from HintLog, ScoresLog, and FailuresLog, when a level is deleted. * Cache invalidation performed on level deletion for records containing levels. * Moved all awaitable queries into a vector which are now executed via AsyncMysqlConnection::multiQuery(). --- src/controllers/AdminController.php | 60 ++++++++++++++----- .../modals/ActionModalController.php | 22 +++++++ src/models/HintLog.php | 22 ++++++- src/models/Level.php | 46 +++++++++++++- src/models/ScoreLog.php | 18 ++++++ src/static/js/admin.js | 20 +++++++ 6 files changed, 169 insertions(+), 19 deletions(-) diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index 7a226213..a7eb52e8 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -1593,14 +1593,26 @@ class= $quiz_status_off_id = 'fb--levels--level-'.strval($quiz->getId()).'-status--off'; - $quiz_id = 'quiz_id'.strval($quiz->getId()); + $quiz_id = strval($quiz->getId()); + $quiz_id_txt = 'quiz_id'.strval($quiz->getId()); $countries_select = await $this->genGenerateCountriesSelect($quiz->getEntityId()); + $delete_button = + ; + $adminsections->appendChild(
      -
      + {tr('EDIT')} - + {$delete_button} @@ -1923,7 +1933,19 @@ class= $flag_status_off_id = 'fb--levels--level-'.strval($flag->getId()).'-status--off'; - $flag_id = 'flag_id'.strval($flag->getId()); + $flag_id_txt = 'flag_id'.strval($flag->getId()); + $flag_id = strval($flag->getId()); + + $delete_button = + ; $attachments_div =
      @@ -2108,7 +2130,7 @@ class="fb-cta cta--red" $adminsections->appendChild(
      - + {tr('EDIT')} - + {$delete_button} @@ -2447,7 +2467,19 @@ class= $base_status_off_id = 'fb--levels--level-'.strval($base->getId()).'-status--off'; - $base_id = 'base_id'.strval($base->getId()); + $base_id = strval($base->getId()); + $base_id_txt = 'base_id'.strval($base->getId()); + + $delete_button = + ; $attachments_div =
      @@ -2635,7 +2667,7 @@ class="fb-cta cta--red" $adminsections->appendChild(
      - + {tr('EDIT')} - + {$delete_button} diff --git a/src/controllers/modals/ActionModalController.php b/src/controllers/modals/ActionModalController.php index 989eb9ab..195a8427 100644 --- a/src/controllers/modals/ActionModalController.php +++ b/src/controllers/modals/ActionModalController.php @@ -99,6 +99,28 @@ private function getModal(string $modal): (:xhp, :xhp) {
      ; return tuple($title, $content); + case 'delete-level': + $title = +

      + {tr('delete_')}{tr('Level')} +

      ; + $content = +
      +

      + {tr( + 'Are you sure you want to delete this level? All data for this level will be irreversibly removed, including scores.', + )} +

      + +
      ; + return tuple($title, $content); case 'logout': $title =

      diff --git a/src/models/HintLog.php b/src/models/HintLog.php index a20fbd30..9e8d1dc5 100644 --- a/src/models/HintLog.php +++ b/src/models/HintLog.php @@ -152,7 +152,7 @@ private static function hintlogFromRow(Map $row): HintLog { } } - // Get all scores. + // Get all hints. public static async function genAllHints(): Awaitable> { $db = await self::genDb(); $result = await $db->queryf('SELECT * FROM hints_log ORDER BY ts DESC'); @@ -165,7 +165,7 @@ private static function hintlogFromRow(Map $row): HintLog { return $hints; } - // Get all scores by team. + // Get all hints by team. public static async function genAllHintsByTeam( int $team_id, ): Awaitable> { @@ -182,4 +182,22 @@ private static function hintlogFromRow(Map $row): HintLog { return $hints; } + + // Get all hints by level. + public static async function genAllHintsByLevel( + int $level_id, + ): Awaitable> { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT * FROM hints_log WHERE level_id = %d', + $level_id, + ); + + $hints = array(); + foreach ($result->mapRows() as $row) { + $hints[] = self::hintlogFromRow($row); + } + + return $hints; + } } diff --git a/src/models/Level.php b/src/models/Level.php index 50f66278..0a10fd60 100644 --- a/src/models/Level.php +++ b/src/models/Level.php @@ -645,9 +645,51 @@ private static function levelFromRow(Map $row): Level { $level = await self::gen($level_id); await Country::genSetUsed($level->getEntityId(), false); - await $db->queryf('DELETE FROM levels WHERE id = %d LIMIT 1', $level_id); + // Remove team points for level + $scores = await ScoreLog::genAllScoresByLevel($level_id); + $level_delete_queries = Vector {}; + foreach ($scores as $score) { + $team_id = $score->getTeamId(); + $points = $score->getPoints(); + $level_delete_queries->add( + sprintf( + 'UPDATE teams SET points = points - %d WHERE id = %d', + $points, + $team_id, + ), + ); + } - self::invalidateMCRecords(); // Invalidate Memcached Level data. + // Remove hint penalties from teams points for level + $hints = await HintLog::genAllHintsByLevel($level_id); + foreach ($hints as $hint) { + $team_id = $hint->getTeamId(); + $penalty = $hint->getPenalty(); + $level_delete_queries->add( + sprintf( + 'UPDATE teams SET points = points + %d WHERE id = %d', + $penalty, + $team_id, + ), + ); + } + + // Delete all references to level + $level_delete_queries->addAll( + Set { + sprintf('DELETE FROM levels WHERE id = %d LIMIT 1', $level_id), + sprintf('DELETE FROM hints_log WHERE level_id = %d', $level_id), + sprintf('DELETE FROM scores_log WHERE level_id = %d', $level_id), + sprintf('DELETE FROM failures_log WHERE level_id = %d', $level_id), + }, + ); + await $db->multiQuery($level_delete_queries); + + self::invalidateMCRecords(); + Control::invalidateMCRecords(); + MultiTeam::invalidateMCRecords(); + HintLog::invalidateMCRecords(); + ScoreLog::invalidateMCRecords(); } // Enable or disable level by passing 1 or 0. diff --git a/src/models/ScoreLog.php b/src/models/ScoreLog.php index c611e40c..1c534847 100644 --- a/src/models/ScoreLog.php +++ b/src/models/ScoreLog.php @@ -202,6 +202,24 @@ private static function scorelogFromRow(Map $row): ScoreLog { return $scores; } + // Get all scores by level. + public static async function genAllScoresByLevel( + int $level_id, + ): Awaitable> { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT * FROM scores_log WHERE level_id = %d', + $level_id, + ); + + $scores = array(); + foreach ($result->mapRows() as $row) { + $scores[] = self::scorelogFromRow($row); + } + + return $scores; + } + // Log successful score. public static async function genLogValidScore( int $level_id, diff --git a/src/static/js/admin.js b/src/static/js/admin.js index 4a811847..69973c52 100644 --- a/src/static/js/admin.js +++ b/src/static/js/admin.js @@ -46,6 +46,15 @@ function deleteTeamPopup(team_id) { sendAdminRequest(delete_team, true); } +//Confirm level deletion +function deleteLevelPopup(level_id) { + var delete_level = { + action: 'delete_level', + level_id: level_id + }; + sendAdminRequest(delete_level, true); +} + // Reset the database function resetDatabase() { var reset_database = { @@ -1402,6 +1411,17 @@ module.exports = { }); }); + // prompt delete level + $('.js-delete-level').on('click', function(event) { + event.preventDefault(); + var level_id = $(this).prev('input').attr('value'); + Modal.loadPopup('p=action&modal=delete-level', 'action-delete-level', function() { + $('#delete_level').click(function() { + deleteLevelPopup(level_id); + }); + }); + }); + // prompt logout $('.js-prompt-logout').on('click', function(event) { event.preventDefault(); From b487fc119f3cef02b289e13d29b80272c4d94c2a Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Thu, 3 Aug 2017 20:57:22 -0400 Subject: [PATCH 15/18] Provision Streamlined, Quick Setup Added, and Multiple Containers Support (#535) * Separate docker containers per service * Provision Streamlined, Quick Setup Added, and Multiple Containers Support * The project now includes a number of "Quick Setup" options to ease the installation or startup process of the platform. The following Quick Setup modes are available: * Direct Installation - Used when directly installing to the system you are on; this is useful when installing on bare metal, an existing VM, or a cloud-based host. * `source ./extra/lib.sh` * `quick_setup install ` * Multi-Server Direct Installation - Used when directly installing the platform with each service on a separate system; this is useful when installing on bare metal systems, existing VMs, or cloud-based hosts. * Database Server (MySQL) * `source ./extra/lib.sh` * `quick_setup install_multi_mysql ` * Cache Server (Memcached) * `source ./extra/lib.sh` * `quick_setup install_multi_nginx ` * HHVM Server (HHVM) * `source ./extra/lib.sh` * `quick_setup install_multi_hhvm ` * Web Server (Nginx) * `source ./extra/lib.sh` * `quick_setup install_multi_nginx ` * Standard Docker Startup - Used when running FBCTF as a single docker container. * `source ./extra/lib.sh` * `quick_setup start_docker ` * Multi-Container Docker Startup - Used when running FBCTF on docker with each service hosted in a separate docker container. * `source ./extra/lib.sh` * `quick_setup start_docker_multi ` * Standard Vagrant Startup - Used when running FBCTF as a single vagrant container. * `source ./extra/lib.sh` * `quick_setup start_docker ` * Multi-Container Vagrant Startup - Used when running FBCTF on vagrant with each service hosted in a separate vagrant container. * `source ./extra/lib.sh` * `quick_setup start_docker_multi ` * Each installation platform now supports both Production Mode (prod) and Development Mode (dev). * The `provision.sh` script has been streamlined and organized based on the services being installed. The installation process now also includes more logging and error handling. Common and core functionally has been migrated to `lib.sh` where appropriate. Color coding has been added to the various output to make quick visual monitoring of the process easier. * Package installation, specifically the check for existing packages has been updated to fix an issue where packages would sometimes not be installed if a similarly named package was already present on the system. * The `provision.sh` script now supports separate installations for each service using the `--multiple-servers` and `--server-type` options. * HHVM configuration has been updated to run HHVM as a network-service. * Nginx configuration is now included in the platform code base and utilized. * Docker service startup scripts are included for each of the services: * `./extra/mysql/mysql_startup.sh` * `./extra/hhvm/hhvm_startup.sh` * `./extra/nginx/nginx_startup.sh` * This PR fixes the docker installation dependencies issue #534. * This PR includes docker-compose configurations for multi-docker containers, fixing issue #440. * Services on Docker (both single container and multi-container) are now monitored to ensure they do not fail. * This PR updates HHVM to the latest stable version for Ubuntu 14.04, HHVM Version 3.18.1, fixing issue #496. * Attachment/Upload permissions have been corrected across the installation environments. This fixes issues with improper permissions on Docker and Vagrant while still enforcing secure file permissions. This should resolve issues like #280 going forward. * Implemented more strict permissions on he CTF PATH (755 verses 777). * Fixed long-standing, upstream induced, HHVM socket permission issues (like #229), mostly experienced in Docker or after a restart (resulting in a _502 Bad Gateway_): https://github.com/facebook/hhvm/issues/6336. Note that this fix is a temporary workaround until the upstream issue is resolved. * With the introduction of the latest available version of HHVM and the inclusion of multiple-server support, performance increases should be noticeable. This should help alleviate issues like #456. * This PR was derived, in part, from PR #530. * Added Memcached Service Restart to container service script * Added logging of PHP/HHVM version to provision script. * Added logging of PHP Alternatives to provision script. * Composer is now installed with the HHVM binary instead of PHP. * Composer Install is run with the HHVM binary instead of PHP. * The Travis trusty Ubuntu image has been downgraded from `sugilite` to `connie`. * Updated run_tests.sh to have write permissions to settings.ini * Set run_tests.sh to use localhost for DB and MC. * HHVM 3.18+ enforces \HH\FormatString - Invariant calls now are of \HH\FormatString type - All `invariant()` calls that are passing in a variable argument have been updated to use literal strings for the format string. Invariant passes the second (and subsequent) arguments to `sprintf()`. The second parameter of `invariant()` must be a literal string, containing placeholders when needed. More information can be found here: hhvm/user-documentation#448. This change ensures the code is strict compliant in HHVM versions 3.18 --- .travis.yml | 2 + Dockerfile | 15 +- Vagrantfile | 2 +- Vagrantfile-multi | 56 ++++ Vagrantfile-single | 16 + docker-compose.yml | 49 +++ extra/cache/Dockerfile | 17 + extra/cache/cache_startup.sh | 11 + extra/hhvm.conf | 1 + extra/hhvm/Dockerfile | 17 + extra/hhvm/hhvm_startup.sh | 15 + extra/lib.sh | 180 +++++++--- extra/motd-ctf.sh | 0 extra/mysql/Dockerfile | 17 + extra/mysql/mysql_startup.sh | 10 + extra/nginx/Dockerfile | 18 + extra/nginx/nginx.conf | 63 ++++ extra/nginx/nginx_startup.sh | 14 + extra/provision.sh | 317 ++++++++++++------ extra/run_tests.sh | 7 +- extra/service_startup.sh | 19 +- extra/settings.ini.example | 6 +- src/Utils.php | 11 +- .../modals/ActionModalController.php | 2 +- src/models/Attachment.php | 11 + 25 files changed, 685 insertions(+), 191 deletions(-) create mode 100644 Vagrantfile-multi create mode 100644 Vagrantfile-single create mode 100644 docker-compose.yml create mode 100644 extra/cache/Dockerfile create mode 100755 extra/cache/cache_startup.sh create mode 100644 extra/hhvm/Dockerfile create mode 100755 extra/hhvm/hhvm_startup.sh mode change 100644 => 100755 extra/motd-ctf.sh create mode 100644 extra/mysql/Dockerfile create mode 100755 extra/mysql/mysql_startup.sh create mode 100644 extra/nginx/Dockerfile create mode 100644 extra/nginx/nginx.conf create mode 100755 extra/nginx/nginx_startup.sh diff --git a/.travis.yml b/.travis.yml index 4a76f949..cc8b03a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ language: generic sudo: required dist: trusty +group: deprecated-2017Q2 + install: ./extra/provision.sh -m dev -s $TRAVIS_BUILD_DIR -d $TRAVIS_BUILD_DIR script: ./extra/run_tests.sh $TRAVIS_BUILD_DIR diff --git a/Dockerfile b/Dockerfile index a23a1dd6..d97cd8cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,14 +12,7 @@ ARG CRT WORKDIR $HOME COPY . $HOME -RUN apt-get update \ - && apt-get install -y \ - rsync \ - curl \ - ca-certificates \ - && chown www-data:www-data $HOME \ - && ./extra/provision.sh -m $MODE -c $TYPE -k $KEY -C $CRT -D $DOMAIN -e $EMAIL -s `pwd` --docker \ - && rm -f /var/run/hhvm/sock \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -CMD ["./extra/service_startup.sh"] +RUN chown www-data:www-data $HOME + +RUN ./extra/provision.sh -m $MODE -c $TYPE -k $KEY -C $CRT -D $DOMAIN -e $EMAIL -s `pwd` --docker +CMD ["./extra/service_startup.sh"] \ No newline at end of file diff --git a/Vagrantfile b/Vagrantfile index ac168729..2078262c 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -6,7 +6,7 @@ VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = "ubuntu/trusty64" config.vm.network "private_network", ip: "10.10.10.5" - config.vm.hostname = "facebookCTF-Dev" + config.vm.hostname = "FacebookCTF-Dev" config.ssh.shell = "bash -c 'BASH_ENV=/etc/profile exec bash'" config.vm.provision "shell", path: "extra/provision.sh", args: ENV['FBCTF_PROVISION_ARGS'], privileged: false config.vm.provider "virtualbox" do |v| diff --git a/Vagrantfile-multi b/Vagrantfile-multi new file mode 100644 index 00000000..158d4923 --- /dev/null +++ b/Vagrantfile-multi @@ -0,0 +1,56 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +VAGRANTFILE_API_VERSION = "2" + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + config.vm.box = "ubuntu/trusty64" + config.ssh.shell = "bash -c 'BASH_ENV=/etc/profile exec bash'" + + # MySQL Server + config.vm.define "mysql" do |mysql| + mysql.vm.network "private_network", ip: "10.10.10.6" + mysql.vm.hostname = "mysql" + mysql.vm.provision "shell", path: "extra/provision.sh", args: "ENV['FBCTF_PROVISION_ARGS'] --multiple-servers --server-type mysql", privileged: false + mysql.vm.provider "virtualbox" do |v| + v.memory = 1024 + v.cpus = 2 + end + end + + # Cache Server + config.vm.define "cache" do |cache| + cache.vm.network "private_network", ip: "10.10.10.8" + cache.vm.hostname = "cache" + cache.vm.provision "shell", path: "extra/provision.sh", args: "ENV['FBCTF_PROVISION_ARGS'] --multiple-servers --server-type cache", privileged: false + cache.vm.provider "virtualbox" do |v| + v.memory = 1024 + v.cpus = 2 + end + end + + # HHVM Server + config.vm.define "hhvm" do |hhvm| + hhvm.vm.network "private_network", ip: "10.10.10.7" + hhvm.vm.hostname = "hhvm" + hhvm.vm.provision "shell", path: "extra/provision.sh", args: "ENV['FBCTF_PROVISION_ARGS'] --multiple-servers --server-type hhvm --mysql-server 10.10.10.6 --cache-server 10.10.10.8", privileged: false + hhvm.vm.provider "virtualbox" do |v| + v.memory = 1024 + v.cpus = 2 + end + end + + # Nginx Server + config.vm.define "nginx" do |nginx| + nginx.vm.network "private_network", ip: "10.10.10.5" + nginx.vm.network "forwarded_port", guest: 80, host: 80 + nginx.vm.network "forwarded_port", guest: 443, host: 443 + nginx.vm.hostname = "nginx" + nginx.vm.provision "shell", path: "extra/provision.sh", args: "ENV['FBCTF_PROVISION_ARGS'] --multiple-servers --server-type nginx --hhvm-server 10.10.10.7", privileged: false + nginx.vm.provider "virtualbox" do |v| + v.memory = 1024 + v.cpus = 2 + end + end + +end diff --git a/Vagrantfile-single b/Vagrantfile-single new file mode 100644 index 00000000..ac168729 --- /dev/null +++ b/Vagrantfile-single @@ -0,0 +1,16 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +VAGRANTFILE_API_VERSION = "2" + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + config.vm.box = "ubuntu/trusty64" + config.vm.network "private_network", ip: "10.10.10.5" + config.vm.hostname = "facebookCTF-Dev" + config.ssh.shell = "bash -c 'BASH_ENV=/etc/profile exec bash'" + config.vm.provision "shell", path: "extra/provision.sh", args: ENV['FBCTF_PROVISION_ARGS'], privileged: false + config.vm.provider "virtualbox" do |v| + v.memory = 4096 + v.cpus = 4 + end +end diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..20ac869c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +version: '2' +services: + mysql: + restart: always + build: + context: . + dockerfile: extra/mysql/Dockerfile + #args: + # MODE: prod + environment: + MYSQL_ROOT_PASSWORD: root + expose: + - "3306" + + cache: + restart: always + build: + context: . + dockerfile: extra/cache/Dockerfile + #args: + # MODE: prod + expose: + - "11211" + + hhvm: + restart: always + build: + context: . + dockerfile: extra/hhvm/Dockerfile + #args: + # MODE: prod + depends_on: + - mysql + - cache + expose: + - "9000" + + nginx: + restart: always + build: + context: . + dockerfile: extra/nginx/Dockerfile + #args: + # MODE: prod + depends_on: + - hhvm + ports: + - "80:80" + - "443:443" diff --git a/extra/cache/Dockerfile b/extra/cache/Dockerfile new file mode 100644 index 00000000..288b9d18 --- /dev/null +++ b/extra/cache/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:trusty +LABEL maintainer="Boik Su " + +ENV HOME /root + +ARG DOMAIN +ARG EMAIL +ARG MODE=dev +ARG TYPE=self +ARG KEY +ARG CRT + +WORKDIR $HOME +COPY . $HOME + +RUN ./extra/provision.sh -m $MODE -c $TYPE -k $KEY -C $CRT -D $DOMAIN -e $EMAIL -s `pwd` --docker --multiple-servers --server-type cache +CMD ["./extra/cache/cache_startup.sh"] diff --git a/extra/cache/cache_startup.sh b/extra/cache/cache_startup.sh new file mode 100755 index 00000000..bd1a508e --- /dev/null +++ b/extra/cache/cache_startup.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +service memcached restart + +while true; do + sleep 5 + + service memcached status +done diff --git a/extra/hhvm.conf b/extra/hhvm.conf index 26d2dd46..6e497da8 100644 --- a/extra/hhvm.conf +++ b/extra/hhvm.conf @@ -9,6 +9,7 @@ hhvm.enable_xhp = true hhvm.force_hh = true hhvm.server.type = fastcgi hhvm.server.ip = 127.0.0.1 +hhvm.server.port = 9000 hhvm.server.file_socket = /var/run/hhvm/sock hhvm.server.default_document = index.php hhvm.server.upload.upload_max_file_size = 25M diff --git a/extra/hhvm/Dockerfile b/extra/hhvm/Dockerfile new file mode 100644 index 00000000..534f1181 --- /dev/null +++ b/extra/hhvm/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:trusty +LABEL maintainer="Boik Su " + +ENV HOME /root + +ARG DOMAIN +ARG EMAIL +ARG MODE=dev +ARG TYPE=self +ARG KEY +ARG CRT + +WORKDIR $HOME +COPY . $HOME + +RUN ./extra/provision.sh -m $MODE -c $TYPE -k $KEY -C $CRT -D $DOMAIN -e $EMAIL -s `pwd` --docker --multiple-servers --server-type hhvm --mysql-server mysql --cache-server cache +CMD ["./extra/hhvm/hhvm_startup.sh"] diff --git a/extra/hhvm/hhvm_startup.sh b/extra/hhvm/hhvm_startup.sh new file mode 100755 index 00000000..a3671250 --- /dev/null +++ b/extra/hhvm/hhvm_startup.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +service hhvm restart + +while true; do + if [[ -e /var/run/hhvm/sock ]]; then + chown www-data:www-data /var/run/hhvm/sock + fi + + sleep 5 + + service hhvm status +done diff --git a/extra/lib.sh b/extra/lib.sh index c6bff3d6..3e862a15 100755 --- a/extra/lib.sh +++ b/extra/lib.sh @@ -4,19 +4,26 @@ # function log() { - echo "[+] $1" + echo "[+] $@" +} + +function print_blank_lines() { + for i in {1..10} + do + echo + done } function error_log() { RED='\033[0;31m' NORMAL='\033[0m' - echo "${RED} [!] $1 ${NORMAL}" + echo -e "${RED} [!] $1 ${NORMAL}" } function ok_log() { GREEN='\033[0;32m' NORMAL='\033[0m' - echo "${GREEN} [+] $1 ${NORMAL}" + echo -e "${GREEN} [+] $1 ${NORMAL}" } function dl() { @@ -30,8 +37,13 @@ function dl() { fi } +function package_repo_update() { + log "Running apt-get update" + sudo DEBIAN_FRONTEND=noninteractive apt-get update +} + function package() { - if [[ -n "$(dpkg --get-selections | grep $1)" ]]; then + if [[ -n "$(dpkg --get-selections | grep -P '^$1\s')" ]]; then log "$1 is already installed. skipping." else log "Installing $1" @@ -40,22 +52,19 @@ function package() { } function install_unison() { - log "Installing Unison 2.48.4" cd / curl -sL https://www.archlinux.org/packages/extra/x86_64/unison/download/ | sudo tar Jx } function repo_osquery() { log "Adding osquery repository keys" - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 1484120AC4E9F8A1A577AEEE97A80C63C9D8B80B - sudo add-apt-repository "deb [arch=amd64] https://osquery-packages.s3.amazonaws.com/trusty trusty main" + sudo DEBIAN_FRONTEND=noninteractive apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 1484120AC4E9F8A1A577AEEE97A80C63C9D8B80B + sudo DEBIAN_FRONTEND=noninteractive add-apt-repository "deb [arch=amd64] https://osquery-packages.s3.amazonaws.com/trusty trusty main" } function install_mysql() { local __pwd=$1 - log "Installing MySQL" - echo "mysql-server-5.5 mysql-server/root_password password $__pwd" | sudo debconf-set-selections echo "mysql-server-5.5 mysql-server/root_password_again password $__pwd" | sudo debconf-set-selections package mysql-server @@ -71,7 +80,6 @@ function set_motd() { if [[ -f /etc/update-motd.d/51/cloudguest ]]; then sudo chmod -x /etc/update-motd.d/51-cloudguest fi - sudo cp "$__path/extra/motd-ctf.sh" /etc/update-motd.d/10-help-text } @@ -86,7 +94,7 @@ function run_grunt() { # properly updated when developing 'remotely' with unison. # grunt watch might take up to 5 seconds to update a file, # give it some time while you are developing. - if [[ $__mode = "dev" ]]; then + if [[ "$__mode" = "dev" ]]; then grunt watch & fi } @@ -108,18 +116,18 @@ function letsencrypt_cert() { dl "https://dl.eff.org/certbot-auto" /usr/bin/certbot-auto sudo chmod a+x /usr/bin/certbot-auto - if [[ $__email == "none" ]]; then + if [[ "$__email" == "none" ]]; then read -p ' -> What is the email for the SSL Certificate recovery? ' __myemail else __myemail=$__email fi - if [[ $__domain == "none" ]]; then + if [[ "$__domain" == "none" ]]; then read -p ' -> What is the domain for the SSL Certificate? ' __mydomain else __mydomain=$__domain fi - if [[ $__docker = true ]]; then + if [[ "$__docker" = true ]]; then cat <<- EOF > /root/tmp/certbot.sh #!/bin/bash if [[ ! ( -d /etc/letsencrypt && "\$(ls -A /etc/letsencrypt)" ) ]]; then @@ -153,17 +161,19 @@ function install_nginx() { local __email=$4 local __domain=$5 local __docker=$6 + local __multiservers=$7 + local __hhvmserver=$8 local __certs_path="/etc/nginx/certs" log "Deploying certificates" sudo mkdir -p "$__certs_path" - if [[ $__mode = "dev" ]]; then + if [[ "$__mode" = "dev" ]]; then local __cert="$__certs_path/dev.crt" local __key="$__certs_path/dev.key" self_signed_cert "$__cert" "$__key" - elif [[ $__mode = "prod" ]]; then + elif [[ "$__mode" = "prod" ]]; then local __cert="$__certs_path/fbctf.crt" local __key="$__certs_path/fbctf.key" case "$__certs" in @@ -174,7 +184,7 @@ function install_nginx() { own_cert "$__cert" "$__key" ;; certbot) - if [[ $__docker = true ]]; then + if [[ "$__docker" = true ]]; then self_signed_cert "$__cert" "$__key" fi letsencrypt_cert "$__cert" "$__key" "$__email" "$__domain" "$__docker" @@ -193,14 +203,20 @@ function install_nginx() { __dhparam="/etc/nginx/certs/dhparam.pem" sudo openssl dhparam -out "$__dhparam" 2048 - cat "$__path/extra/nginx.conf" | sed "s|CTFPATH|$__path/src|g" | sed "s|CER_FILE|$__cert|g" | sed "s|KEY_FILE|$__key|g" | sed "s|DHPARAM_FILE|$__dhparam|g" | sudo tee /etc/nginx/sites-available/fbctf.conf + if [[ "$__multiservers" == true ]]; then + cat "$__path/extra/nginx/nginx.conf" | sed "s|CTFPATH|$__path/src|g" | sed "s|CER_FILE|$__cert|g" | sed "s|KEY_FILE|$__key|g" | sed "s|DHPARAM_FILE|$__dhparam|g" | sed "s|HHVMSERVER|$__hhvmserver|g" | sudo tee /etc/nginx/sites-available/fbctf.conf + else + cat "$__path/extra/nginx.conf" | sed "s|CTFPATH|$__path/src|g" | sed "s|CER_FILE|$__cert|g" | sed "s|KEY_FILE|$__key|g" | sed "s|DHPARAM_FILE|$__dhparam|g" | sudo tee /etc/nginx/sites-available/fbctf.conf + fi sudo rm -f /etc/nginx/sites-enabled/default sudo ln -sf /etc/nginx/sites-available/fbctf.conf /etc/nginx/sites-enabled/fbctf.conf - # Restart nginx - sudo nginx -t - sudo service nginx restart + if [[ "$__multiservers" == false ]]; then + # Restart nginx + sudo nginx -t + sudo service nginx restart + fi } # TODO: We should split this function into one where the repo is added, and a @@ -208,41 +224,44 @@ function install_nginx() { function install_hhvm() { local __path=$1 local __config=$2 + local __multiservers=$3 package software-properties-common log "Adding HHVM key" - sudo apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0x5a16e7281be7a449 + sudo DEBIAN_FRONTEND=noninteractive apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0x5a16e7281be7a449 log "Adding HHVM repo" - sudo add-apt-repository "deb http://dl.hhvm.com/ubuntu $(lsb_release -sc) main" + sudo DEBIAN_FRONTEND=noninteractive add-apt-repository "deb http://dl.hhvm.com/ubuntu $(lsb_release -sc) main" + + package_repo_update log "Installing HHVM" - sudo apt-get update # Installing the package so the dependencies are installed too package hhvm - # The HHVM package version 3.15 is broken and crashes. See: https://github.com/facebook/hhvm/issues/7333 - # Until this is fixed, install manually closest previous version, 3.14.5 - sudo apt-get remove hhvm -y - # Clear old files - sudo rm -Rf /var/run/hhvm/* - sudo rm -Rf /var/cache/hhvm/* - local __package="hhvm_3.14.5~$(lsb_release -sc)_amd64.deb" - dl "http://dl.hhvm.com/ubuntu/pool/main/h/hhvm/$__package" "/tmp/$__package" - sudo dpkg -i "/tmp/$__package" + log "Enabling HHVM to start by default" + sudo update-rc.d hhvm defaults log "Copying HHVM configuration" - cat "$__path/extra/hhvm.conf" | sed "s|CTFPATH|$__path/|g" | sudo tee "$__config" + if [[ "$__multiservers" == true ]]; then + cat "$__path/extra/hhvm.conf" | sed "s|CTFPATH|$__path/|g" | sed "s|hhvm.server.ip|;hhvm.server.ip|g" | sed "s|hhvm.server.file_socket|;hhvm.server.file_socket|g" | sudo tee "$__config" + else + cat "$__path/extra/hhvm.conf" | sed "s|CTFPATH|$__path/|g" | sed "s|hhvm.server.port|;hhvm.server.port|g" | sudo tee "$__config" + fi log "HHVM as PHP systemwide" sudo /usr/bin/update-alternatives --install /usr/bin/php php /usr/bin/hhvm 60 - log "Enabling HHVM to start by default" - sudo update-rc.d hhvm defaults + log "PHP Alternaives:" + sudo /usr/bin/update-alternatives --display php - log "Restart HHVM" + log "Restarting HHVM" sudo service hhvm restart + + log "PHP/HHVM Version:" + php -v + hhvm --version } function hhvm_performance() { @@ -251,19 +270,18 @@ function hhvm_performance() { local __oldrepo="/var/run/hhvm/hhvm.hhbc" local __repofile="/var/cache/hhvm/hhvm.hhbc" - log "Enabling HHVM RepoAuthoritative mode" cat "$__config" | sed "s|$__oldrepo|$__repofile|g" | sudo tee "$__config" sudo hhvm-repo-mode enable "$__path" sudo chown www-data:www-data "$__repofile" + sudo service hhvm start } function install_composer() { local __path=$1 - log "Installing composer" cd $__path curl -sS https://getcomposer.org/installer | php - php composer.phar install + hhvm composer.phar install sudo mv composer.phar /usr/bin sudo chmod +x /usr/bin/composer.phar } @@ -276,6 +294,7 @@ function import_empty_db() { local __db=$3 local __path=$4 local __mode=$5 + local __multiservers=$6 log "Creating DB - $__db" mysql -u "$__user" --password="$__pwd" -e "CREATE DATABASE IF NOT EXISTS \`$__db\`;" @@ -288,13 +307,15 @@ function import_empty_db() { mysql -u "$__user" --password="$__pwd" "$__db" -e "source $__path/database/logos.sql;" log "Creating user..." - mysql -u "$__user" --password="$__pwd" -e "CREATE USER '$__u'@'localhost' IDENTIFIED BY '$__p';" || true # don't fail if the user exists - mysql -u "$__user" --password="$__pwd" -e "GRANT ALL PRIVILEGES ON \`$__db\`.* TO '$__u'@'localhost';" + if [[ "$__multiservers == true" ]]; then + mysql -u "$__user" --password="$__pwd" -e "CREATE USER '$__u'@'%' IDENTIFIED BY '$__p';" || true # don't fail if the user exists + mysql -u "$__user" --password="$__pwd" -e "GRANT ALL PRIVILEGES ON \`$__db\`.* TO '$__u'@'%';" + else + mysql -u "$__user" --password="$__pwd" -e "CREATE USER '$__u'@'localhost' IDENTIFIED BY '$__p';" || true # don't fail if the user exists + mysql -u "$__user" --password="$__pwd" -e "GRANT ALL PRIVILEGES ON \`$__db\`.* TO '$__u'@'localhost';" + fi mysql -u "$__user" --password="$__pwd" -e "FLUSH PRIVILEGES;" - log "DB Connection file" - cat "$__path/extra/settings.ini.example" | sed "s/DATABASE/$__db/g" | sed "s/MYUSER/$__u/g" | sed "s/MYPWD/$__p/g" > "$__path/settings.ini" - local PASSWORD log "Adding default admin user" if [[ $__mode = "dev" ]]; then @@ -303,8 +324,17 @@ function import_empty_db() { PASSWORD=$(head -c 500 /dev/urandom | md5sum | cut -d" " -f1) fi - set_password "$PASSWORD" "$__user" "$__pwd" "$__db" "$__path" - log "The password for admin is: $PASSWORD" + set_password "$PASSWORD" "$__user" "$__pwd" "$__db" "$__path" "$__multiservers" + + print_blank_lines + ok_log "The password for admin is: $PASSWORD" + if [[ "$__multiservers" == true ]]; then + echo + ok_log "Please note password as it will not be displayed again..." + echo + sleep 10 + fi + print_blank_lines } function set_password() { @@ -313,11 +343,16 @@ function set_password() { local __db_pwd=$3 local __db=$4 local __path=$5 + local __multiservers=$6 - HASH=$(hhvm -f "$__path/extra/hash.php" "$__admin_pwd") + if [[ "$__multiservers" == true ]]; then + HASH=$(php "$__path/extra/hash.php" "$__admin_pwd") + else + HASH=$(hhvm -f "$__path/extra/hash.php" "$__admin_pwd") + fi # First try to delete the existing admin user - mysql -u "$__user" --password="$__db_pwd" "$__db" -e "DELETE FROM teams WHERE name='admin' AND admin=1" + mysql -u "$__user" --password="$__db_pwd" "$__db" -e "DELETE FROM teams WHERE name='admin' AND admin=1;" # Then insert the new admin user with ID 1 (just as a convention, we shouldn't rely on this in the code) mysql -u "$__user" --password="$__db_pwd" "$__db" -e "INSERT INTO teams (id, name, password_hash, admin, protected, logo, created_ts) VALUES (1, 'admin', '$HASH', 1, 1, 'admin', NOW());" @@ -348,7 +383,7 @@ function update_repo() { log "Configuring git to ignore permission changes" git -C "$CTF_PATH/" config core.filemode false log "Setting permissions" - sudo chmod -R 777 "$__ctf_path/" + sudo chmod -R 755 "$__ctf_path/" fi fi @@ -357,3 +392,48 @@ function update_repo() { run_grunt "$__ctf_path" "$__mode" } + +function quick_setup() { + local __type=$1 + local __mode=$2 + local __ip=$3 + local __ip2=$4 + + if [[ "$__type" = "install" ]]; then + ./extra/provision.sh -m $__mode -s $PWD + elif [[ "$__type" = "install_multi_mysql" ]]; then + ./extra/provision.sh -m $__mode -s $PWD --multiple-servers --server-type mysql + elif [[ "$__type" = "install_multi_hhvm" ]]; then + ./extra/provision.sh -m $__mode -s $PWD --multiple-servers --server-type hhvm --mysql-server $__ip --cache-server $__ip2 + elif [[ "$__type" = "install_multi_nginx" ]]; then + ./extra/provision.sh -m $__mode -s $PWD --multiple-servers --server-type nginx --hhvm-server $__ip + elif [[ "$__type" = "install_multi_cache" ]]; then + ./extra/provision.sh -m $__mode -s $PWD --multiple-servers --server-type cache + elif [[ "$__type" = "start_docker" ]]; then + package_repo_update + package docker-ce + sudo docker build --build-arg MODE=$__mode -t="fbctf-image" . + sudo docker run --name fbctf -p 80:80 -p 443:443 fbctf-image + elif [[ "$__type" = "start_docker_multi" ]]; then + package_repo_update + package python-pip + sudo pip install docker-compose + if [[ "$__mode" = "prod" ]]; then + sed -i -e 's| # MODE: prod| MODE: prod|g' ./docker-compose.yml + sed -i -e 's| #args| args|g' ./docker-compose.yml + elif [[ "$__mode" = "dev" ]]; then + sed -i -e 's| MODE: prod| # MODE: prod|g' ./docker-compose.yml + sed -i -e 's| args| #args|g' ./docker-compose.yml + fi + sudo docker-compose up + elif [[ "$__type" = "start_vagrant" ]]; then + cp Vagrantfile-single Vagrantfile + export FBCTF_PROVISION_ARGS="-m $__mode" + vagrant up + elif [[ "$__type" = "start_vagrant_multi" ]]; then + cp Vagrantfile-multi Vagrantfile + export FBCTF_PROVISION_ARGS="-m $__mode" + vagrant up + fi +} + diff --git a/extra/motd-ctf.sh b/extra/motd-ctf.sh old mode 100644 new mode 100755 diff --git a/extra/mysql/Dockerfile b/extra/mysql/Dockerfile new file mode 100644 index 00000000..6f23ff8b --- /dev/null +++ b/extra/mysql/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:trusty +LABEL maintainer="Boik Su " + +ENV HOME /root + +ARG DOMAIN +ARG EMAIL +ARG MODE=dev +ARG TYPE=self +ARG KEY +ARG CRT + +WORKDIR $HOME +COPY . $HOME + +RUN ./extra/provision.sh -m $MODE -c $TYPE -k $KEY -C $CRT -D $DOMAIN -e $EMAIL -s `pwd` --docker --multiple-servers --server-type mysql +CMD ["./extra/mysql/mysql_startup.sh"] diff --git a/extra/mysql/mysql_startup.sh b/extra/mysql/mysql_startup.sh new file mode 100755 index 00000000..f72893c4 --- /dev/null +++ b/extra/mysql/mysql_startup.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +service mysql restart + +while true; do + sleep 5 + service mysql status +done diff --git a/extra/nginx/Dockerfile b/extra/nginx/Dockerfile new file mode 100644 index 00000000..4fa27f92 --- /dev/null +++ b/extra/nginx/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:trusty +LABEL maintainer="Boik Su " + +ENV HOME /root + +ARG DOMAIN +ARG EMAIL +ARG MODE=dev +ARG TYPE=self +ARG KEY +ARG CRT + +WORKDIR $HOME +COPY . $HOME +RUN chown www-data:www-data $HOME + +RUN ./extra/provision.sh -m $MODE -c $TYPE -k $KEY -C $CRT -D $DOMAIN -e $EMAIL -s `pwd` --docker --multiple-servers --server-type nginx --hhvm-server hhvm +CMD ["./extra/nginx/nginx_startup.sh"] diff --git a/extra/nginx/nginx.conf b/extra/nginx/nginx.conf new file mode 100644 index 00000000..350ba270 --- /dev/null +++ b/extra/nginx/nginx.conf @@ -0,0 +1,63 @@ +# Do not send nginx version number in error pages or server header +server_tokens off; + +add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self'; frame-src 'self'; object-src 'none'"; + +server { + listen 80; + rewrite ^ https://$host$request_uri? permanent; +} + +server { + listen 443; + + ssl on; + ssl_certificate CER_FILE; + ssl_certificate_key KEY_FILE; + + ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; + + ssl_dhparam DHPARAM_FILE; + + ssl_session_cache shared:SSL:10m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_stapling on; + ssl_stapling_verify on; + resolver 8.8.8.8 8.8.4.4 valid=300s; + resolver_timeout 5s; + + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; + + add_header Cache-Control "no-cache, no-store"; + add_header Pragma "no-cache"; + expires -1; + + root CTFPATH; + index index.php; + + location /data/attachments/ { + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_pass HHVMSERVER:9000; + } + + location /data/customlogos/ { + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_pass HHVMSERVER:9000; + } + + location ~ \.php$ { + try_files $uri =404; + fastcgi_pass HHVMSERVER:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + error_page 400 401 402 403 404 500 /error.php; + client_max_body_size 25M; +} + diff --git a/extra/nginx/nginx_startup.sh b/extra/nginx/nginx_startup.sh new file mode 100755 index 00000000..98c95b1c --- /dev/null +++ b/extra/nginx/nginx_startup.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +if [[ -e /root/tmp/certbot.sh ]]; then + /bin/bash /root/tmp/certbot.sh +fi + +service nginx restart + +while true; do + sleep 5 + service nginx status +done diff --git a/extra/provision.sh b/extra/provision.sh index acda8b0f..108a64dd 100755 --- a/extra/provision.sh +++ b/extra/provision.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# fbctf provisioning script +# FBCTF provisioning script # # Usage: provision.sh [-h|--help] [PARAMETER [ARGUMENT]] [PARAMETER [ARGUMENT]] ... # @@ -12,7 +12,7 @@ # Arguments for MODE: # dev Provision will run in development mode. Certificate will be self-signed. # prod Provision will run in production mode. -# update Provision will update fbctf running in the machine. +# update Provision will update FBCTF running in the machine. # # Arguments for TYPE: # self Provision will use a self-signed SSL certificate that will be generated. @@ -20,14 +20,19 @@ # certbot Provision will generate a SSL certificate using letsencrypt/certbot. More info here: https://certbot.eff.org/ # # Optional Parameters: -# -U, --update Pull from master GitHub branch and sync files to fbctf folder. -# -R, --no-repo-mode Disables HHVM Repo Authoritative mode in production mode. -# -k PATH, --keyfile PATH Path to supplied SSL key file. -# -C PATH, --certfile PATH Path to supplied SSL certificate pem file. -# -D DOMAIN, --domain DOMAIN Domain for the SSL certificate to be generated using letsencrypt. -# -e EMAIL, --email EMAIL Domain for the SSL certificate to be generated using letsencrypt. -# -s PATH, --code PATH Path to fbctf code. -# -d PATH, --destination PATH Destination path to place the fbctf folder. +# -U, --update Pull from master GitHub branch and sync files to fbctf folder. +# -R, --no-repo-mode Disables HHVM Repo Authoritative mode in production mode. +# -k PATH, --keyfile PATH Path to supplied SSL key file. +# -C PATH, --certfile PATH Path to supplied SSL certificate pem file. +# -D DOMAIN, --domain DOMAIN Domain for the SSL certificate to be generated using letsencrypt. +# -e EMAIL, --email EMAIL Domain for the SSL certificate to be generated using letsencrypt. +# -s PATH, --code PATH Path to fbctf code. +# -d PATH, --destination PATH Destination path to place the fbctf folder. +# --multiple-servers Utilize multiple servers for installation. Server must be specified with --server-type +# --server-type SERVER Server to provision. 'hhvm', 'nginx', 'mysql', or 'cache' can be used. +# --hhvm-server SERVER HHVM Server IP when utilizing multiple servers. Call from 'nginx' server container. +# --mysql-server SERVER MySQL Server IP when utilizing multiple servers. Call from 'hhvm' server container. +# --cache-server SERVER Memcached Server IP when utilizing multiple servers. Call from 'hhvm' server container. # # Examples: # Provision fbctf in development mode: @@ -56,6 +61,12 @@ EMAIL="none" CODE_PATH="/vagrant" CTF_PATH="/var/www/fbctf" HHVM_CONFIG_PATH="/etc/hhvm/server.ini" +DOCKER=false +MULTIPLE_SERVERS=false +SERVER_TYPE="none" +HHVM_SERVER="hhvm" +MYSQL_SERVER="mysql" +CACHE_SERVER="cache" # Arrays with valid arguments VALID_MODE=("dev" "prod") @@ -69,32 +80,37 @@ function usage() { printf " -m MODE, --mode MODE \tMode of operation. Default value is dev\n" printf " -c TYPE, --cert TYPE \tType of certificate to use. Default value is self\n" printf "\nArguments for MODE:\n" - printf " dev \tProvision will run in development mode. Certificate will be self-signed.\n" - printf " prod \tProvision will run in production mode.\n" - printf " update \tProvision will update fbctf running in the machine.\n" + printf " dev \tProvision will run in Development mode. Certificate will be self-signed.\n" + printf " prod \tProvision will run in Production mode.\n" + printf " update \tProvision will update FBCTF running in the machine.\n" printf "\nArguments for TYPE:\n" printf " self \tProvision will use a self-signed SSL certificate that will be generated.\n" printf " own \tProvision will use the SSL certificate provided by the user.\n" printf " certbot Provision will generate a SSL certificate using letsencrypt/certbot. More info here: https://certbot.eff.org/\n" printf "\nOptional Parameters:\n" - printf " -U, --update \t\tPull from master GitHub branch and sync files to fbctf folder.\n" - printf " -R, --no-repo-mode \tDisables HHVM Repo Authoritative mode in production mode.\n" - printf " -k PATH, --keyfile PATH \tPath to supplied SSL key file.\n" - printf " -C PATH, --certfile PATH \tPath to supplied SSL certificate pem file.\n" - printf " -D DOMAIN, --domain DOMAIN \tDomain for the SSL certificate to be generated using letsencrypt.\n" - printf " -e EMAIL, --email EMAIL \tDomain for the SSL certificate to be generated using letsencrypt.\n" - printf " -s PATH, --code PATH \t\tPath to fbctf code. Default is /vagrant\n" - printf " -d PATH, --destination PATH \tDestination path to place the fbctf folder. Default is /var/www/fbctf\n" + printf " -U --update \t\tPull from master GitHub branch and sync files to fbctf folder.\n" + printf " -R --no-repo-mode \tDisables HHVM Repo Authoritative mode in production mode.\n" + printf " -k PATH --keyfile PATH \tPath to supplied SSL key file.\n" + printf " -C PATH --certfile PATH \tPath to supplied SSL certificate pem file.\n" + printf " -D DOMAIN --domain DOMAIN \tDomain for the SSL certificate to be generated using letsencrypt.\n" + printf " -e EMAIL --email EMAIL \tDomain for the SSL certificate to be generated using letsencrypt.\n" + printf " -s PATH --code PATH \t\tPath to fbctf code. Default is /vagrant\n" + printf " -d PATH --destination PATH \tDestination path to place the fbctf folder. Default is /var/www/fbctf\n" + printf " --multiple-servers --utilize multiple servers for installation. Server must be specified with -st\n" + printf " --server-type SERVER --specify server to provision. 'hhvm', 'nginx', 'mysql', or 'cache' can be used.\n" + printf " --hhvm-server SERVER --specify HHVM Server IP when utilizing multiple servers. Call from 'nginx' container.\n" + printf " --mysql-server SERVER --specify MySQL Server IP when utilizing multiple servers. Call from 'hhvm' container.\n" + printf " --cache-server SERVER --memcached Server IP when utilizing multiple servers. Call from 'hhvm' server container.\n" printf "\nExamples:\n" - printf " Provision fbctf in development mode:\n" + printf " Provision FBCTF in development mode:\n" printf "\t%s -m dev -s /home/foobar/fbctf -d /var/fbctf\n" "${0}" - printf " Provision fbctf in production mode using my own certificate:\n" + printf " Provision FBCTF in production mode using my own certificate:\n" printf "\t%s -m prod -c own -k /etc/certs/my.key -C /etc/certs/cert.crt -s /home/foobar/fbctf -d /var/fbctf\n" "${0}" - printf " Update current fbctf in development mode, having code in /home/foobar/fbctf and running from /var/fbctf:\n" + printf " Update current FBCTF in development mode, having code in /home/foobar/fbctf and running from /var/fbctf:\n" printf "\t%s -m dev -U -s /home/foobar/fbctf -d /var/fbctf\n" "${0}" } -ARGS=$(getopt -n "$0" -o hm:c:URk:C:D:e:s:d: -l "help,mode:,cert:,update,repo-mode,keyfile:,certfile:,domain:,email:,code:,destination:,docker" -- "$@") +ARGS=$(getopt -n "$0" -o hm:c:URk:C:D:e:s:d: -l "help,mode:,cert:,update,repo-mode,keyfile:,certfile:,domain:,email:,code:,destination:,docker,multiple-servers,server-type:,hhvm-server:,mysql-server:,cache-server:" -- "$@") eval set -- "$ARGS" @@ -160,6 +176,26 @@ while true; do DOCKER=true shift ;; + --multiple-servers) + MULTIPLE_SERVERS=true + shift + ;; + --server-type) + SERVER_TYPE=$2 + shift 2 + ;; + --hhvm-server) + HHVM_SERVER=$2 + shift 2 + ;; + --mysql-server) + MYSQL_SERVER=$2 + shift 2 + ;; + --cache-server) + CACHE_SERVER=$2 + shift 2 + ;; --) shift break @@ -171,17 +207,17 @@ while true; do esac done +# Source library script for subprocesses source "$CODE_PATH/extra/lib.sh" -# Install git first -package git +package_repo_update -# Are we just updating a running fbctf? -if [[ "$UPDATE" == true ]] ; then - update_repo "$MODE" "$CODE_PATH" "$CTF_PATH" - exit 0 -fi +package git +package curl +package wget +package rsync +# Check for available memory, should be over 1GB AVAILABLE_RAM=`free -mt | grep Total | awk '{print $2}'` if [ $AVAILABLE_RAM -lt 1024 ]; then @@ -190,13 +226,7 @@ if [ $AVAILABLE_RAM -lt 1024 ]; then sleep 5 fi -log "Provisioning in $MODE mode" -log "Using $TYPE certificate" -log "Source code folder $CODE_PATH" -log "Destination folder $CTF_PATH" - -# We only create a new directory and rsync files over if it's different from the -# original code path +# We only create a new directory and rsync files over if it's different from the original code path if [[ "$CODE_PATH" != "$CTF_PATH" ]]; then log "Creating code folder $CTF_PATH" [[ -d "$CTF_PATH" ]] || sudo mkdir -p "$CTF_PATH" @@ -209,94 +239,163 @@ if [[ "$CODE_PATH" != "$CTF_PATH" ]]; then log "Configuring git to ignore permission changes" git -C "$CTF_PATH/" config core.filemode false log "Setting permissions" - sudo chmod -R 777 "$CTF_PATH/" + sudo chmod -R 755 "$CTF_PATH/" fi fi -# There we go! +# If multiple servers are being utilized, ensure provision was called from the "nginx" or "hhvm" servers + if [[ "$MULTIPLE_SERVERS" == false || "$SERVER_TYPE" = "nginx" || $SERVER_TYPE = "hhvm" ]]; then + package language-pack-en -# Ascii art is always appreciated -set_motd "$CTF_PATH" + if [[ "$UPDATE" == true ]] ; then + log "Updating repo" + update_repo "$MODE" "$CODE_PATH" "$CTF_PATH" + exit 0 + fi -# Some Ubuntu distros don't come with curl installed -package curl + log "Provisioning in $MODE mode" + log "Using $TYPE certificate" + log "Source code folder $CODE_PATH" + log "Destination folder $CTF_PATH" + + log "Setting Message of the Day (MOTD)" + set_motd "$CTF_PATH" + + # If multiple servers are being utilized, ensure provision was called from the "hhvm" server + if [[ "$MULTIPLE_SERVERS" == false || "$SERVER_TYPE" = "hhvm" ]]; then + log "Installing HHVM" + install_hhvm "$CTF_PATH" "$HHVM_CONFIG_PATH" "$MULTIPLE_SERVERS" + + # Install Composer + log "Installing Composer" + install_composer "$CTF_PATH" + log "Installing Composer in /usr/bin" + hhvm /usr/bin/composer.phar install + + # In production, enable HHVM Repo Authoritative mode by default. + # More info here: https://docs.hhvm.com/hhvm/advanced-usage/repo-authoritative + if [[ "$MODE" == "prod" ]] && [[ "$NOREPOMODE" == false ]]; then + log "Enabling HHVM Repo Authoritative Mode" + hhvm_performance "$CTF_PATH" "$HHVM_CONFIG_PATH" + else + log "HHVM Repo Authoritative mode NOT enabled" + fi + + log "Creating DB Connection file" + if [[ $MULTIPLE_SERVERS == true ]]; then + cat "$CTF_PATH/extra/settings.ini.example" | sed "s/DBHOST/$MYSQL_SERVER/g" | sed "s/DATABASE/$DB/g" | sed "s/MYUSER/$U/g" | sed "s/MYPWD/$P/g" | sed "s/MCHOST/$CACHE_SERVER/g" | sudo tee "$CTF_PATH/settings.ini" + else + cat "$CTF_PATH/extra/settings.ini.example" | sed "s/DBHOST/127.0.0.1/g" | sed "s/DATABASE/$DB/g" | sed "s/MYUSER/$U/g" | sed "s/MYPWD/$P/g" | sed "s/MCHOST/127.0.0.1/g" | sudo tee "$CTF_PATH/settings.ini" + fi + fi -# We only run this once so provisioning is faster -sudo apt-get update + # If multiple servers are being utilized, ensure provision was called from the "nginx" server + if [[ "$MULTIPLE_SERVERS" == false || "$SERVER_TYPE" = "nginx" ]]; then + # Packages to be installed in Dev mode + if [[ "$MODE" == "dev" ]]; then + package build-essential + package libssl-dev + package python-all-dev + package python-setuptools + package python-pip + log "Upgrading pip" + sudo -H pip install --upgrade pip + log "Installing pip - mycli" + sudo -H pip install mycli + package emacs + package htop + fi + + package ca-certificates + package npm + log "Updating npm" + sudo npm install -g npm@lts + + package nodejs-legacy + + log "Installing all required npm node_modules" + sudo npm install --prefix "$CTF_PATH" + sudo npm install -g grunt + sudo npm install -g flow-bin + + log "Running grunt to generate JS files" + run_grunt "$CTF_PATH" "$MODE" + + log "Installing nginx and certificates" + install_nginx "$CTF_PATH" "$MODE" "$TYPE" "$EMAIL" "$DOMAIN" "$DOCKER" "$MULTIPLE_SERVERS" "$HHVM_SERVER" + + log "Installing unison 2.48.3. Remember to install the same version on your host machine" + package xz-utils + install_unison + fi -# Some people need this language pack installed or HHVM will report errors -package language-pack-en + log "Creating attachments folder, and setting ownership to www-data" + sudo sudo mkdir -p "$CTF_PATH/src/data/attachments" + sudo sudo mkdir -p "$CTF_PATH/src/data/attachments/deleted" + sudo chown -R www-data:www-data "$CTF_PATH/src/data/attachments" + sudo chown -R www-data:www-data "$CTF_PATH/src/data/attachments/deleted" -# Packages to be installed in dev mode -if [[ "$MODE" == "dev" ]]; then - sudo apt-get install -y build-essential python-all-dev python-setuptools - package python-pip - sudo -H pip install --upgrade pip - sudo -H pip install mycli - package emacs - package htop + log "Creating custom logos folder, and setting ownership to www-data" + sudo mkdir -p "$CTF_PATH/src/data/customlogos" + sudo chown -R www-data:www-data "$CTF_PATH/src/data/customlogos" fi -# Install memcached -package memcached - -# Install MySQL -install_mysql "$P_ROOT" - -# Install HHVM -install_hhvm "$CTF_PATH" "$HHVM_CONFIG_PATH" - -# Install Composer -install_composer "$CTF_PATH" -# This step has done `cd "$CTF_PATH"` -composer.phar install - -# In production, enable HHVM Repo Authoritative mode by default. -# More info here: https://docs.hhvm.com/hhvm/advanced-usage/repo-authoritative -if [[ "$MODE" == "prod" ]] && [[ "$NOREPOMODE" == false ]]; then - hhvm_performance "$CTF_PATH" "$HHVM_CONFIG_PATH" -else - log "HHVM Repo Authoritative mode NOT enabled" +# If multiple servers are being utilized, ensure provision was called from the "cache" server +if [[ "$MULTIPLE_SERVERS" == false || "$SERVER_TYPE" = "cache" ]]; then + # Install Memcached + package memcached + + # If cache server is running standalone, enable memcached for all interfaces. + if [[ "$MULTIPLE_SERVERS" == true ]]; then + sudo sed -i 's/^-l/#-l/g' /etc/memcached.conf + sudo service memcached restart + else + sudo sed -i 's/^#-l/-l/g' /etc/memcached.conf + sudo service memcached restart + fi fi -# Install and update NPM -package npm -# Update NPM with itself: https://github.com/npm/npm/issues/14610 -sudo npm install -g npm@lts - -# Install node -package nodejs-legacy - -# Install all required node_modules in the CTF folder -sudo npm install --prefix "$CTF_PATH" -sudo npm install -g grunt -sudo npm install -g flow-bin - -# Run grunt to generate JS files -run_grunt "$CTF_PATH" "$MODE" +# If multiple servers are being utilized, ensure provision was called from the "mysql" server +if [[ "$MULTIPLE_SERVERS" == false || "$SERVER_TYPE" = "mysql" ]]; then + log "Installing MySQL" + install_mysql "$P_ROOT" -# Install nginx and certificates -install_nginx "$CTF_PATH" "$MODE" "$TYPE" "$EMAIL" "$DOMAIN" "$DOCKER" + # Configuration for MySQL + if [[ "$MULTIPLE_SERVERS" == true ]] && [[ "$SERVER_TYPE" = "mysql" ]]; then + # This is required in order to generate password hash (since HHVM is not being installed) + package php5-cli -# Install unison 2.48.3 -install_unison -log "Remember install the same version of unison (2.48.3) in your host machine" + sudo sed -e '/^bind-address/ s/^#*/#/' -i /etc/mysql/my.cnf + sudo sed -e '/^skip-external-locking/ s/^#*/#/' -i /etc/mysql/my.cnf + fi -# Database creation -import_empty_db "root" "$P_ROOT" "$DB" "$CTF_PATH" "$MODE" - -# Make attachments folder world writable -sudo chmod 777 "$CTF_PATH/src/data/attachments" -sudo chmod 777 "$CTF_PATH/src/data/attachments/deleted" -# Make custom logos folder, and make it world writable -sudo mkdir -p "$CTF_PATH/src/data/customlogos" -sudo chmod 777 "$CTF_PATH/src/data/customlogos" + # Database creation + log "Creating database" + import_empty_db "root" "$P_ROOT" "$DB" "$CTF_PATH" "$MODE" "$MULTIPLE_SERVERS" +fi # Display the final message, depending on the context -if [[ -d "/vagrant" ]]; then - log 'fbctf deployment is complete! Ready in https://10.10.10.5' +if [[ "$MULTIPLE_SERVERS" == true ]]; then + if [[ "$DOCKER" == true ]]; then + : + else + if [[ "$SERVER_TYPE" = "hhvm" ]]; then + sudo service hhvm restart + elif [[ "$SERVER_TYPE" = "nginx" ]]; then + sudo service nginx restart + if [[ -d "/vagrant" ]]; then + ok_log 'FBCTF deployment is complete! Cleaning up... FBCTF will be Ready at https://10.10.10.5' + fi + elif [[ "$SERVER_TYPE" = "mysql" ]]; then + sudo service mysql restart + elif [[ "$SERVER_TYPE" = "cache" ]]; then + sudo service memcached restart + fi + fi +elif [[ -d "/vagrant" ]]; then + ok_log 'FBCTF deployment is complete! Cleaning up... FBCTF will be Ready at https://10.10.10.5' else - ok_log 'fbctf deployment is complete!' + ok_log 'FBCTF deployment is complete! Cleaning up...' fi exit 0 diff --git a/extra/run_tests.sh b/extra/run_tests.sh index 719680a9..862fbe4f 100755 --- a/extra/run_tests.sh +++ b/extra/run_tests.sh @@ -22,11 +22,12 @@ mysql -u "$DB_USER" --password="$DB_PWD" "$DB" -e "source $CODE_PATH/database/co if [ -f "$CODE_PATH/settings.ini" ]; then echo "[+] Backing up existing settings.ini" - cp "$CODE_PATH/settings.ini" "$CODE_PATH/settings.ini.bak" + sudo cp "$CODE_PATH/settings.ini" "$CODE_PATH/settings.ini.bak" fi +# Because this is a test suite we assume you are running on a single server, if not update the DB and MC addresses... echo "[+] DB Connection file" -cat "$CODE_PATH/extra/settings.ini.example" | sed "s/DATABASE/$DB/g" | sed "s/MYUSER/$DB_USER/g" | sed "s/MYPWD/$DB_PWD/g" > "$CODE_PATH/settings.ini" +cat "$CODE_PATH/extra/settings.ini.example" | sed "s/DATABASE/$DB/g" | sed "s/MYUSER/$DB_USER/g" | sed "s/MYPWD/$DB_PWD/g" | sed "s/DBHOST/127.0.0.1/g" | sed "s/MCHOST/127.0.0.1/g" | sudo tee "$CODE_PATH/settings.ini" echo "[+] Starting tests" hhvm vendor/phpunit/phpunit/phpunit tests @@ -37,7 +38,7 @@ mysql -u "$DB_USER" --password="$DB_PWD" -e "FLUSH PRIVILEGES;" if [ -f "$CODE_PATH/settings.ini.bak" ]; then echo "[+] Restoring previous settings.ini" - mv "$CODE_PATH/settings.ini.bak" "$CODE_PATH/settings.ini" + sudo mv "$CODE_PATH/settings.ini.bak" "$CODE_PATH/settings.ini" fi # In the future, we should use the hh_client exit status. diff --git a/extra/service_startup.sh b/extra/service_startup.sh index ace2434c..14df5340 100755 --- a/extra/service_startup.sh +++ b/extra/service_startup.sh @@ -6,17 +6,24 @@ if [[ -e /root/tmp/certbot.sh ]]; then /bin/bash /root/tmp/certbot.sh fi +if [[ -e /var/run/hhvm/sock ]]; then + rm -f /var/run/hhvm/sock +fi + service hhvm restart service nginx restart service mysql restart service memcached restart -chown www-data:www-data /var/run/hhvm/sock - while true; do - if [[ -e /var/log/nginx/access.log ]]; then - exec tail -F /var/log/nginx/access.log - else - exec sleep 10 + if [[ -e /var/run/hhvm/sock ]]; then + chown www-data:www-data /var/run/hhvm/sock fi + + sleep 5 + + service hhvm status + service nginx status + service mysql status + service memcached status done diff --git a/extra/settings.ini.example b/extra/settings.ini.example index e8078b22..5b0cb752 100644 --- a/extra/settings.ini.example +++ b/extra/settings.ini.example @@ -1,12 +1,12 @@ ; This is a sample configuration file -DB_HOST = '127.0.0.1' +DB_HOST = 'DBHOST' DB_PORT = '3306' DB_NAME = 'DATABASE' DB_USERNAME = 'MYUSER' DB_PASSWORD = 'MYPWD' -MC_HOST = '127.0.0.1' +MC_HOST = 'MCHOST' MC_PORT = '11211' -GOOGLE_OAUTH_FILE = '' \ No newline at end of file +GOOGLE_OAUTH_FILE = '' diff --git a/src/Utils.php b/src/Utils.php index 1802d80a..a0747f2a 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -7,10 +7,7 @@ function must_have_idx(?KeyedContainer $arr, Tk $idx): Tv { invariant($arr !== null, 'Container is null'); $result = idx($arr, $idx); - invariant( - $result !== null, - sprintf('Index %s not found in container', $idx), - ); + invariant($result !== null, 'Index %s not found in container', $idx); return $result; } @@ -19,7 +16,7 @@ function must_have_string( Tk $idx, ): string { $result = must_have_idx($arr, $idx); - invariant(is_string($result), "Expected $idx to be a string"); + invariant(is_string($result), 'Expected %s to be a string', strval($idx)); return $result; } @@ -28,7 +25,7 @@ function must_have_int( Tk $idx, ): int { $result = must_have_idx($arr, $idx); - invariant(is_int($result), "Expected $idx to be an int"); + invariant(is_int($result), 'Expected %s to be an int', strval($idx)); return $result; } @@ -37,7 +34,7 @@ function must_have_bool( Tk $idx, ): bool { $result = must_have_idx($arr, $idx); - invariant(is_bool($result), "Expected $idx to be a bool"); + invariant(is_bool($result), 'Expected %s to be a bool', strval($idx)); return $result; } diff --git a/src/controllers/modals/ActionModalController.php b/src/controllers/modals/ActionModalController.php index 195a8427..f8d23fad 100644 --- a/src/controllers/modals/ActionModalController.php +++ b/src/controllers/modals/ActionModalController.php @@ -336,7 +336,7 @@ class="fb-cta cta--yellow js-trigger-google-oauth">

      ; return tuple($title, $content); default: - invariant(false, "Invalid modal name $modal"); + invariant(false, 'Invalid modal name %s', strval($modal)); } } diff --git a/src/models/Attachment.php b/src/models/Attachment.php index 6bdddcfd..75cbf6c8 100644 --- a/src/models/Attachment.php +++ b/src/models/Attachment.php @@ -81,6 +81,17 @@ public function getLevelId(): int { $chmod === true, 'Failed to set attachment file permissions to 0600', ); + + // Force ownership to www-data + $chown = chown( + must_have_string($server, 'DOCUMENT_ROOT').$local_filename, + 'www-data', + ); + invariant( + $chown === true, + 'Failed to set attachment file ownership to www-data', + ); + } else { return false; } From e181da25b18c2534ef44f60510e702ef1b9695df Mon Sep 17 00:00:00 2001 From: ubuntu Date: Sat, 5 Aug 2017 02:44:21 -0400 Subject: [PATCH 16/18] Fixed conflict in ScoreLog --- src/models/ScoreLog.php | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/models/ScoreLog.php b/src/models/ScoreLog.php index dc5253fb..1e399a16 100644 --- a/src/models/ScoreLog.php +++ b/src/models/ScoreLog.php @@ -293,33 +293,6 @@ private static function scorelogFromRow(Map $row): ScoreLog { MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. } - public static async function genScoreLogUpdate( - int $level_id, - int $team_id, - int $points, - string $type, - string $timestamp, - ): Awaitable { - $db = await self::genDb(); - await $db->queryf( - 'UPDATE scores_log SET ts = %s, level_id = %d, team_id = %d, points = %d, type = %s WHERE level_id = %d AND team_id = %d', - $timestamp, - $level_id, - $team_id, - $points, - $type, - $level_id, - $team_id, - ); - self::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. - Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control 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. - } - public static async function genUpdateScoreLogBonus( int $level_id, int $team_id, From dd9de6e58e01833d1c3df68032afcb0d866a2818 Mon Sep 17 00:00:00 2001 From: ubuntu Date: Sat, 5 Aug 2017 03:04:08 -0400 Subject: [PATCH 17/18] Fixed conflict in ScoreLog --- src/models/ScoreLog.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/models/ScoreLog.php b/src/models/ScoreLog.php index 1e399a16..1c534847 100644 --- a/src/models/ScoreLog.php +++ b/src/models/ScoreLog.php @@ -293,27 +293,6 @@ private static function scorelogFromRow(Map $row): ScoreLog { MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. } - public static async function genUpdateScoreLogBonus( - int $level_id, - int $team_id, - int $points, - ): Awaitable { - $db = await self::genDb(); - await $db->queryf( - 'UPDATE scores_log SET ts = ts, points = %d WHERE level_id = %d AND team_id = %d', - $points, - $level_id, - $team_id, - ); - self::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. - Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control 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. - } - public static async function genLevelScores( int $level_id, ): Awaitable> { From f7c64396e56f92584e2366552aedb82c6dd19338 Mon Sep 17 00:00:00 2001 From: ubuntu Date: Sat, 5 Aug 2017 03:32:53 -0400 Subject: [PATCH 18/18] Fixed conflict in Control --- src/models/Control.php | 89 ------------------------------------------ 1 file changed, 89 deletions(-) diff --git a/src/models/Control.php b/src/models/Control.php index 3078509e..25d00b8c 100644 --- a/src/models/Control.php +++ b/src/models/Control.php @@ -335,95 +335,6 @@ class Control extends Model { } } - public static async function genAutoBegin(): Awaitable { - // 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()); - - // 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(); - } - } - - public static async function genAutoEnd(): Awaitable { - // 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()); - - // 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(); - } - } - - public static async function genAutoRun(): Awaitable { - // Get start time - $config_game = await Configuration::gen('game'); - $game = intval($config_game->getValue()); - - if ($game === 0) { - // Check and start the game - await Control::genAutoBegin(); - } else { - // Check and stop the game - await Control::genAutoEnd(); - } - } - - public static async function genRunAutoRunScript(): Awaitable { - $autorun_status = await Control::checkScriptRunning('autorun'); - if ($autorun_status === false) { - $autorun_location = escapeshellarg( - must_have_string(Utils::getSERVER(), 'DOCUMENT_ROOT'). - '/scripts/autorun.php', - ); - $cmd = - 'hhvm -vRepo.Central.Path=/var/run/hhvm/.hhvm.hhbc_autorun '. - $autorun_location. - ' > /dev/null 2>&1 & echo $!'; - $pid = shell_exec($cmd); - await Control::genStartScriptLog(intval($pid), 'autorun', $cmd); - } - } - - public static async function checkScriptRunning( - string $name, - ): Awaitable { - $db = await self::genDb(); - $result = await $db->queryf( - 'SELECT pid FROM scripts WHERE name = %s AND status = 1 LIMIT 1', - $name, - ); - 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(); - } - return $status; - } else { - return false; - } - } - public static async function importGame(): Awaitable { $data_game = JSONImporterController::readJSON('game_file'); if (is_array($data_game)) {