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/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/database/schema.sql b/database/schema.sql index da145d61..82a2966b 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -211,6 +211,7 @@ INSERT INTO `configuration` (field, value, description) VALUES("pause_ts", "0", INSERT INTO `configuration` (field, value, description) VALUES("timer", "0", "(Boolean) Timer is enabled"); INSERT INTO `configuration` (field, value, description) VALUES("scoring", "0", "(Boolean) Ability score levels"); INSERT INTO `configuration` (field, value, description) VALUES("gameboard", "1", "(Boolean) Refresh all data in the gameboard"); +INSERT INTO `configuration` (field, value, description) VALUES("auto_announce", "0", "(Boolean) Auto game announcements"); INSERT INTO `configuration` (field, value, description) VALUES("progressive_cycle", "300", "(Integer) Frequency to take progressive scoreboard in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("bases_cycle", "5", "(Integer) Frequency to score base levels in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("autorun_cycle", "30", "(Integer) Frequency to cycle autorun in seconds"); @@ -232,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; @@ -417,3 +419,22 @@ CREATE TABLE `announcements_log` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `activity_log` +-- + +DROP TABLE IF EXISTS `activity_log`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `activity_log` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `subject` text NOT NULL, + `action` text NOT NULL, + `entity` text NOT NULL, + `message` text NOT NULL, + `arguments` text NOT NULL, + `ts` timestamp NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; diff --git a/database/test_schema.sql b/database/test_schema.sql index 122c9ce4..5beadecb 100644 --- a/database/test_schema.sql +++ b/database/test_schema.sql @@ -211,6 +211,7 @@ INSERT INTO `configuration` (field, value, description) VALUES("pause_ts", "0", INSERT INTO `configuration` (field, value, description) VALUES("timer", "0", "(Boolean) Timer is enabled"); INSERT INTO `configuration` (field, value, description) VALUES("scoring", "0", "(Boolean) Ability score levels"); INSERT INTO `configuration` (field, value, description) VALUES("gameboard", "1", "(Boolean) Refresh all data in the gameboard"); +INSERT INTO `configuration` (field, value, description) VALUES("auto_announce", "0", "(Boolean) Auto game announcements"); INSERT INTO `configuration` (field, value, description) VALUES("progressive_cycle", "300", "(Integer) Frequency to take progressive scoreboard in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("bases_cycle", "5", "(Integer) Frequency to score base levels in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("autorun_cycle", "30", "(Integer) Frequency to cycle autorun in seconds"); @@ -231,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; -- @@ -414,3 +419,22 @@ CREATE TABLE `announcements_log` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `activity_log` +-- + +DROP TABLE IF EXISTS `activity_log`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `activity_log` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `subject` text NOT NULL, + `action` text NOT NULL, + `entity` text NOT NULL, + `message` text NOT NULL, + `arguments` text NOT NULL, + `ts` timestamp NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; 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 0144aee7..862fbe4f 100755 --- a/extra/run_tests.sh +++ b/extra/run_tests.sh @@ -20,8 +20,14 @@ 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" + 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 @@ -30,6 +36,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" + sudo 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" 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/AdminController.php b/src/controllers/AdminController.php index 621a221f..a7eb52e8 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>> @@ -304,6 +305,7 @@ class="fb-cta cta--yellow" 'ldap_domain_suffix' => Configuration::gen('ldap_domain_suffix'), 'scoring' => Configuration::gen('scoring'), 'gameboard' => Configuration::gen('gameboard'), + 'auto_announce' => Configuration::gen('auto_announce'), 'timer' => Configuration::gen('timer'), 'progressive_cycle' => Configuration::gen('progressive_cycle'), 'default_bonus' => Configuration::gen('default_bonus'), @@ -315,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'), }; @@ -334,6 +337,7 @@ class="fb-cta cta--yellow" $ldap_domain_suffix = $results['ldap_domain_suffix']; $scoring = $results['scoring']; $gameboard = $results['gameboard']; + $auto_announce = $results['auto_announce']; $timer = $results['timer']; $progressive_cycle = $results['progressive_cycle']; $default_bonus = $results['default_bonus']; @@ -345,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'; @@ -364,6 +369,8 @@ class="fb-cta cta--yellow" $scoring_off = $scoring->getValue() === '0'; $gameboard_on = $gameboard->getValue() === '1'; $gameboard_off = $gameboard->getValue() === '0'; + $auto_announce_on = $auto_announce->getValue() === '1'; + $auto_announce_off = $auto_announce->getValue() === '0'; $timer_on = $timer->getValue() === '1'; $timer_off = $timer->getValue() === '0'; $livesync_on = $livesync->getValue() === '1'; @@ -799,6 +806,29 @@ class="icon--badge" name="fb--conf--autorun_cycle" /> +
+ +
+ + + + +
+
@@ -1047,11 +1077,21 @@ class="icon--badge"
- + + getValue()} + /> +
+
+
+
+ getValue()} + name="fb--conf--custom_byline" + value={$custom_byline->getValue()} />
@@ -1112,7 +1152,7 @@ class="icon--badge" return
-

{tr('Game Controls')}

+

{tr('Announcement Controls')}

{tr('status_')}{tr('OK')} @@ -1553,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} @@ -1883,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 =
@@ -2068,7 +2130,7 @@ class="fb-cta cta--red" $adminsections->appendChild(
- + {tr('EDIT')} - + {$delete_button} @@ -2407,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 =
@@ -2595,7 +2667,7 @@ class="fb-cta cta--red" $adminsections->appendChild(
- + {tr('EDIT')} - + {$delete_button} 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 ; return tuple($title, $content); + case 'delete-level': + $title = +

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

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

@@ -314,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/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/language/lang_basque.php b/src/language/lang_basque.php index 8cfa7835..044122ff 100644 --- a/src/language/lang_basque.php +++ b/src/language/lang_basque.php @@ -85,6 +85,12 @@ 'Pasahitza', 'Choose an Emblem' => 'Aukeratu ikur bat', + 'or upload your own' => + 'edo zure igo', + 'Clear your custom emblem to use a default emblem.' => + 'Garbitu zure pertsonalizatua ikurra ikur lehenetsia erabiltzeko.', + 'Password is too simple' => + 'Pasahitza sinpleegia da', 'Sign Up' => 'Izena eman', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Hasi Time', 'Expected End Time' => 'Espero zen End Time', + 'Internationalization' => + 'Nazioartekotzea', 'Language' => 'Hizkuntza', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Logotipo Pertsonalizatua', + 'Logo' => + 'Logotipo', + 'Custom Text' => + 'Testua Pertsonalizatua', 'DELETE' => 'eZABATU', 'Delete' => @@ -247,10 +263,26 @@ 'General', 'Back Up Database' => 'Backup Database', - 'Export Game' => - 'Export Game', - 'Import Game' => - 'Inportatu Game', + 'Export Full Game' => + 'Export Full Game', + 'Import Full Game' => + 'Inportazio Full Game', + 'Import Teams' => + 'Inportazio Taldeak', + 'Export Teams' => + 'Export Taldeak', + 'Import Logos' => + 'Inportazio Logos', + 'Export Logos' => + 'Export Logos', + 'Import Levels' => + 'Inportazio Mailak', + 'Export Levels' => + 'Export Mailak', + 'Import Categories' => + 'Inportazio Kategoriak', + 'Export Categories' => + 'Export Kategoriak', 'Levels' => 'Mailak', 'New Quiz Level' => diff --git a/src/language/lang_bp.php b/src/language/lang_bp.php index 89fed047..bfd346a8 100644 --- a/src/language/lang_bp.php +++ b/src/language/lang_bp.php @@ -85,6 +85,12 @@ 'Senha', 'Choose an Emblem' => 'Escolha um Emblema', + 'or upload your own' => + 'ou carregue o seu próprio', + 'Clear your custom emblem to use a default emblem.' => + 'Limpe seu emblema personalizado para usar um emblema padrão.', + 'Password is too simple' => + 'A senha é muito simples', 'Sign Up' => 'Registrar', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Tempo Inicial', 'Expected End Time' => 'Tempo Final Esperado', + 'Internationalization' => + 'Internacionalização', 'Language' => 'Idioma', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Logotipo personalizado', + 'Logo' => + 'Logotipo', + 'Custom Text' => + 'Texto personalizado', 'DELETE' => 'DELETAR', 'Delete' => @@ -658,4 +674,12 @@ 'Pule para jogar', 'Powered By Facebook' => 'Powered By Facebook', + 'Active Directory / LDAP' => + 'Active Directory / LDAP', + 'LDAP Server' => + 'LDAP Servidor', + 'LDAP Port' => + 'LDAP Porta', + 'LDAP Domain' => + 'LDAP Domínio', ); diff --git a/src/language/lang_cat.php b/src/language/lang_cat.php index 83385bc4..12034834 100644 --- a/src/language/lang_cat.php +++ b/src/language/lang_cat.php @@ -85,6 +85,12 @@ 'Contrassenya', 'Choose an Emblem' => 'Escollir un emblema', + 'or upload your own' => + 'o pujar el seu propi', + 'Clear your custom emblem to use a default emblem.' => + 'Rebuig seu emblema costum d\'utilitzar un emblema predeterminat.', + 'Password is too simple' => + 'La contrasenya és massa simple', 'Sign Up' => 'Donar-se d\'alta', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Hora de començar!', 'Expected End Time' => 'S\'esperava la fi del Temps', + 'Internationalization' => + 'Internacionalització', 'Language' => 'Idioma', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Logotip Personalitzat', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Text Personalitzat', 'DELETE' => 'ESBORRAR', 'Delete' => diff --git a/src/language/lang_de.php b/src/language/lang_de.php index 2092c796..fbaa950f 100644 --- a/src/language/lang_de.php +++ b/src/language/lang_de.php @@ -84,7 +84,13 @@ 'Password' => 'Kennwort', 'Choose an Emblem' => - 'Wähle ein Symbol aus.', + 'Wähle ein Symbol aus', + 'or upload your own' => + 'oder lade deine eigenen hoch', + 'Clear your custom emblem to use a default emblem.' => + 'Löschen Sie Ihr benutzerdefiniertes Emblem, um ein Standard-Emblem zu verwenden.', + 'Password is too simple' => + 'Passwort ist zu einfach', 'Sign Up' => 'Registrieren', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Startzeitpunkt', 'Expected End Time' => 'Erwarteter Endzeitpunkt', + 'Internationalization' => + 'Internationalisierung', 'Language' => 'Sprache', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Kundenspezifisches Logo', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Kundenspezifischer Text', 'DELETE' => 'LÖSCHEN', 'Delete' => @@ -658,4 +674,12 @@ 'Weiterspielen', 'Powered By Facebook' => 'Powered by Facebook', + 'Active Directory / LDAP' => + 'Active Directory / LDAP', + 'LDAP Server' => + 'LDAP Server', + 'LDAP Port' => + 'LDAP Port', + 'LDAP Domain' => + 'LDAP Domäne', ); diff --git a/src/language/lang_es.php b/src/language/lang_es.php index 5903087b..a552080a 100644 --- a/src/language/lang_es.php +++ b/src/language/lang_es.php @@ -85,6 +85,12 @@ 'Password', 'Choose an Emblem' => 'Elige un Emblema', + 'or upload your own' => + 'o sube tus propios', + 'Clear your custom emblem to use a default emblem.' => + 'Elimine su emblema personalizado para usar un emblema predeterminado.', + 'Password is too simple' => + 'La contraseña es demasiado simple', 'Sign Up' => 'Regístrate', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -232,11 +238,11 @@ 'Internationalization' => 'Internacionalización', 'Language' => - 'Idioma', + 'Lenguaje', 'Branding' => - 'Personalización', + 'Marca', 'Custom Logo' => - 'Logo Personalizado', + 'Logotipo Personalizado', 'Logo' => 'Logo', 'Custom Text' => @@ -257,10 +263,26 @@ 'General', 'Back Up Database' => 'Hacer Copia de Seguridad de la Base de Datos', - 'Export Game' => - 'Exportar Juego', - 'Import Game' => - 'Importar Juego', + 'Export Full Game' => + 'Juego completo de exportación', + 'Import Full Game' => + 'Juego completo de importación', + 'Import Teams' => + 'Equipos de importación', + 'Export Teams' => + 'Exportar Equipos', + 'Import Logos' => + 'Importar Logos', + 'Export Logos' => + 'Exportar Logos', + 'Import Levels' => + 'Niveles de importación', + 'Export Levels' => + 'Niveles de exportación', + 'Import Categories' => + 'Categorías de importación', + 'Export Categories' => + 'Categorías de exportación', 'Levels' => 'Niveles', 'New Quiz Level' => @@ -638,7 +660,7 @@ 'Command_Line', 'Click "Nav" to access main navigation links like Rules of Play, Registration, Blog, Jobs & more.' => 'Presiona "Nav" para acceder a los links principales como Reglas del Juego, Registro, Blog, Trabajo y más.', - 'Track your competition by clicking "scorboard" to access real-time game statistics and graphs.' => + 'Track your competition by clicking "scoreboard" to access real-time game statistics and graphs.' => 'Haz un seguimiento del juego haciendo clic en "scoreboard" para acceder a estadísticas y gráficas en tiempo real.', 'Have fun, be the best and conquer the world.' => 'Diviértete, sé el mejor y conquista el mundo.', diff --git a/src/language/lang_fa.php b/src/language/lang_fa.php index 24d1a036..c7ef75a6 100644 --- a/src/language/lang_fa.php +++ b/src/language/lang_fa.php @@ -85,6 +85,12 @@ 'گذرواژه', 'Choose an Emblem' => 'یک علامت انتخاب کنید', + 'or upload your own' => + 'و یا خود را', + 'Clear your custom emblem to use a default emblem.' => + 'پاک آرم سفارشی خود را به استفاده از یک علامت به طور پیش فرض.', + 'Password is too simple' => + 'گذرواژه خیلی ساده است', 'Sign Up' => 'ثبت‌نام', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'زمان شروع', 'Expected End Time' => 'زمان مورد انتظار برای پایان', + 'Internationalization' => + 'بین المللی کردن', 'Language' => 'زبان', + 'Branding' => + 'نام تجاری', + 'Custom Logo' => + 'لوگو های سفارشی', + 'Logo' => + 'آرم', + 'Custom Text' => + 'متن سفارشی', 'DELETE' => 'حذف', 'Delete' => diff --git a/src/language/lang_fr.php b/src/language/lang_fr.php index 888860ba..fd127a26 100644 --- a/src/language/lang_fr.php +++ b/src/language/lang_fr.php @@ -85,8 +85,14 @@ 'Mot de passe', 'Choose an Emblem' => 'Choisissez un emblème', + 'or upload your own' => + 'ou téléchargez votre propre', + 'Clear your custom emblem to use a default emblem.' => + 'Effacez votre emblème personnalisé pour utiliser un emblème par défaut.', 'Sign Up' => 'S\'inscrire', + 'Password is too simple' => + 'Le mot de passe est trop simple', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => 'Inscrivez-vous ici pour jouer à la capture de drapeau. Une fois inscrit, vous serez connecté.', 'Not Available' => @@ -229,8 +235,18 @@ 'Heure de début', 'Expected End Time' => 'Heure de fin prévue', + 'Internationalization' => + 'Internationalisation', 'Language' => 'Langage', + 'Branding' => + 'l\'image de marque', + 'Custom Logo' => + 'Logo personnalisé', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Texte Personnalisé', 'DELETE' => 'SUPPRIMER', 'Delete' => @@ -453,6 +469,8 @@ 'Date de création', 'Last Access' => 'Dernier accès', + 'Last Page Access' => + 'Dernier accès à la page', 'Data' => 'Données', 'Sessions' => @@ -656,4 +674,12 @@ 'Passer pour jouer', 'Powered By Facebook' => 'Propulsé par Facebook', + 'Active Directory / LDAP' => + 'Active Directory / LDAP', + 'LDAP Server' => + 'LDAP Serveur', + 'LDAP Port' => + 'LDAP Port', + 'LDAP Domain' => + 'LDAP Domaine', ); diff --git a/src/language/lang_hi.php b/src/language/lang_hi.php index 8cecda62..af2b8689 100644 --- a/src/language/lang_hi.php +++ b/src/language/lang_hi.php @@ -1,11 +1,12 @@ 'H:i:s D d/m/Y', //used by date() function //Translations for IndexController 'Facebook CTF' => - ' Facebook CTF', + 'Facebook CTF', 'Conquer the world' => 'दुनिया जीत लो', 'Play' => @@ -84,6 +85,12 @@ 'पासवर्ड', 'Choose an Emblem' => 'एक प्रतीक चुनें', + 'or upload your own' => + 'या अपना खुद का अपलोड करें', + 'Clear your custom emblem to use a default emblem.' => + 'एक डिफ़ॉल्ट प्रतीक का उपयोग करने के लिए अपने कस्टम प्रतीक को साफ़ करें.', + 'Password is too simple' => + 'पासवर्ड बहुत सरल है', 'Sign Up' => 'साइन अप करें', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -228,8 +235,18 @@ 'शुरू होने का समय', 'Expected End Time' => 'अपेक्षित समाप्ति समय', + 'Internationalization' => + 'अंतर्राष्ट्रीयकरण', 'Language' => 'भाषा', + 'Branding' => + 'ब्रांडिंग', + 'Custom Logo' => + 'कस्टम लोगो', + 'Logo' => + 'प्रतीक चिन्ह', + 'Custom Text' => + 'प्रचलित पाठ', 'DELETE' => 'हटाएँ', 'Delete' => @@ -655,4 +672,12 @@ 'खेलने के लिए स्किप करें ', 'Powered By Facebook' => ' Facebook द्वारा संचालित', + 'Active Directory / LDAP' => + 'सक्रिय निर्देशिका / एलडीएपी', + 'LDAP Server' => + 'एलडीएपी सर्वर', + 'LDAP Port' => + 'एलडीएपी पोर्ट', + 'LDAP Domain' => + 'एलडीएपी डोमेन', ); diff --git a/src/language/lang_hu.php b/src/language/lang_hu.php index fcc8883f..ef1f361a 100644 --- a/src/language/lang_hu.php +++ b/src/language/lang_hu.php @@ -79,16 +79,22 @@ 'Token', 'Team Registration' => 'Csapat regisztráció', - 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => - 'Regisztrálj a Capture The Flag versenyre! A regisztráció után a rendszer automatikusan beléptet.', 'Team Name' => 'Csapatnév', 'Password' => 'Jelszó', 'Choose an Emblem' => 'Válassz egy logót', + 'or upload your own' => + 'vagy feltöltheti a sajátját', + 'Clear your custom emblem to use a default emblem.' => + 'Törölje az egyéni emblémát az alapértelmezett embléma használatához.', + 'Password is too simple' => + 'A jelszó túl egyszerű', 'Sign Up' => 'Regisztráció', + 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => + 'Regisztrálj a Capture The Flag versenyre! A regisztráció után a rendszer automatikusan beléptet.', 'Not Available' => 'Nem elérhető', 'Team Registration will be open soon, stay tuned!' => @@ -229,8 +235,18 @@ 'A játék kezdete', 'Expected End Time' => 'A játék várható vége', + 'Internationalization' => + 'Nemzetközivé', 'Language' => 'Nyelv', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Egyéni logó', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Egyéni Szöveg', 'DELETE' => 'TÖRÖL', 'Delete' => @@ -251,6 +267,22 @@ 'Játék exportálása', 'Import Full Game' => 'Játék importálása', + 'Import Teams' => + 'Import csapatok', + 'Export Teams' => + 'Export csapatok', + 'Import Logos' => + 'Import Logos', + 'Export Logos' => + 'Export Logos', + 'Import Levels' => + 'Import szintek', + 'Export Levels' => + 'Export szintek', + 'Import Categories' => + 'Import Kategóriák', + 'Export Categories' => + 'Export Kategóriák', 'Levels' => 'Feladatok', 'New Quiz Level' => diff --git a/src/language/lang_id.php b/src/language/lang_id.php index c2246ec9..6f21c5aa 100644 --- a/src/language/lang_id.php +++ b/src/language/lang_id.php @@ -85,6 +85,12 @@ 'Kata Sandi', 'Choose an Emblem' => 'Pilih Logo', + 'or upload your own' => + 'atau upload sendiri', + 'Clear your custom emblem to use a default emblem.' => + 'Kosongkan lambang kustom Anda untuk menggunakan lambang default.', + 'Password is too simple' => + 'Password terlalu sederhana', 'Sign Up' => 'Daftar Baru', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Jam Mulai', 'Expected End Time' => 'Estimasi Jam Berakhir', + 'Internationalization' => + 'Internasionalisasi', 'Language' => 'Bahasa', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Logo Kustom', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Teks Khusus', 'DELETE' => 'HAPUS', 'Delete' => @@ -453,6 +469,8 @@ 'Waktu Pembuatan', 'Last Access' => 'Diakses Terakhir', + 'Last Page Access' => + 'Halaman Access terakhir', 'Data' => 'Data', 'Sessions' => @@ -656,4 +674,12 @@ 'Langsung bermain', 'Powered By Facebook' => 'Didukung Oleh Facebook', + 'Active Directory / LDAP' => + 'Active Directory / LDAP', + 'LDAP Server' => + 'LDAP Server', + 'LDAP Port' => + 'LDAP Pelabuhan', + 'LDAP Domain' => + 'LDAP Domain', ); diff --git a/src/language/lang_it.php b/src/language/lang_it.php index c5aac7c5..b2b860a6 100644 --- a/src/language/lang_it.php +++ b/src/language/lang_it.php @@ -85,6 +85,12 @@ 'Password', 'Choose an Emblem' => 'Scegli un Emblema', + 'or upload your own' => + 'o caricare il tuo', + 'Clear your custom emblem to use a default emblem.' => + 'Cancellare l\'emblema personalizzato per utilizzare un emblema predefinito.', + 'Password is too simple' => + 'La password è troppo semplice', 'Sign Up' => 'Registrazione', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Ora di Inizio', 'Expected End Time' => 'Ora di Fine Prevista', + 'Internationalization' => + 'Internazionalizzazione', 'Language' => 'Lingua', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Logo personalizzato', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Testo Personalizzato', 'DELETE' => 'ELIMINA', 'Delete' => @@ -658,4 +674,12 @@ 'Vai a Giocare', 'Powered By Facebook' => 'Sviluppato da Facebook', + 'Active Directory / LDAP' => + 'Active Directory / LDAP', + 'LDAP Server' => + 'Server LDAP', + 'LDAP Port' => + 'Port LDAP', + 'LDAP Domain' => + 'Dominio LDAP', ); diff --git a/src/language/lang_ms.php b/src/language/lang_ms.php index 0de7a83f..497be88b 100644 --- a/src/language/lang_ms.php +++ b/src/language/lang_ms.php @@ -85,6 +85,12 @@ 'Kata Laluan', 'Choose an Emblem' => 'Pilih Emblem', + 'or upload your own' => + 'atau muat naik anda sendiri', + 'Clear your custom emblem to use a default emblem.' => + 'Membersihkan lambang adat anda untuk menggunakan lambang lalai.', + 'Password is too simple' => + 'Password adalah terlalu mudah', 'Sign Up' => 'Daftar Baru', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Masa Mula', 'Expected End Time' => 'Jangkaan Masa Tamat', + 'Internationalization' => + 'Pengantarabangsaan', 'Language' => 'Bahasa', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Custom Logo', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Text Custom', 'DELETE' => 'HAPUS', 'Delete' => @@ -658,4 +674,12 @@ 'Langkau untuk bermain', 'Powered By Facebook' => 'Dikuasakan Oleh Facebook', + 'Active Directory / LDAP' => + 'Active Directory / LDAP', + 'LDAP Server' => + 'LDAP Server', + 'LDAP Port' => + 'Port LDAP', + 'LDAP Domain' => + 'LDAP Domain', ); diff --git a/src/language/lang_ru.php b/src/language/lang_ru.php index 947b8aae..960ccd3e 100644 --- a/src/language/lang_ru.php +++ b/src/language/lang_ru.php @@ -85,6 +85,12 @@ 'Пароль', 'Choose an Emblem' => 'Выберите Эмблему', + 'or upload your own' => + 'Или загрузить свои собственные', + 'Clear your custom emblem to use a default emblem.' => + 'Очистите свою эмблему, чтобы использовать эмблему по умолчанию.', + 'Password is too simple' => + 'Пароль слишком прост', 'Sign Up' => 'Зарегистрироваться', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Начало', 'Expected End Time' => 'Ожидаемое Окончание', + 'Internationalization' => + 'интернационализация', 'Language' => 'Язык', + 'Branding' => + 'Брендинг', + 'Custom Logo' => + 'Пользовательский логотип', + 'Logo' => + 'логотип', + 'Custom Text' => + 'Пользовательский текст', 'DELETE' => 'УДАЛИТЬ', 'Delete' => @@ -500,6 +516,8 @@ 'Конец', 'Rank' => 'Ранг', + 'pts' => + 'точки', //points 'Your Rank' => 'Ваш Ранг', 'Your Score' => @@ -656,4 +674,12 @@ 'Пропустить', 'Powered By Facebook' => 'При поддержке Facebook', + 'Active Directory / LDAP' => + 'Active Directory / LDAP', + 'LDAP Server' => + 'Сервер LDAP', + 'LDAP Port' => + 'Порт LDAP', + 'LDAP Domain' => + 'Домен LDAP', ); diff --git a/src/language/lang_sv.php b/src/language/lang_sv.php index 05a69304..2119f896 100644 --- a/src/language/lang_sv.php +++ b/src/language/lang_sv.php @@ -85,6 +85,12 @@ 'Lösenord', 'Choose an Emblem' => 'Välj en lagsymbol', + 'or upload your own' => + 'eller ladda upp din egen', + 'Clear your custom emblem to use a default emblem.' => + 'Rensa ditt anpassade emblem för att använda ett standardemblem.', + 'Password is too simple' => + 'Lösenordet är för enkelt', 'Sign Up' => 'Skapa Konto', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Starttid', 'Expected End Time' => 'Förväntad sluttid', + 'Internationalization' => + 'Internationalisering', 'Language' => 'Språk', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Anpassad logotyp', + 'Logo' => + 'Logotyp', + 'Custom Text' => + 'Anpassad Text', 'DELETE' => 'RADERA', 'Delete' => @@ -453,6 +469,8 @@ 'Skapad', 'Last Access' => 'Senaste användning', + 'Last Page Access' => + 'Sista sidan Åtkomst', 'Data' => 'Data', 'Sessions' => @@ -656,4 +674,12 @@ 'Hoppa till spelet', 'Powered By Facebook' => 'Powered By Facebook', + 'Active Directory / LDAP' => + 'Active Directory / LDAP', + 'LDAP Server' => + 'LDAP Server', + 'LDAP Port' => + 'LDAP Port', + 'LDAP Domain' => + 'LDAP Domain', ); diff --git a/src/language/lang_sw.php b/src/language/lang_sw.php index d3edb1ce..bfa49ac8 100644 --- a/src/language/lang_sw.php +++ b/src/language/lang_sw.php @@ -85,6 +85,12 @@ 'Nenosiri', 'Choose an Emblem' => 'Chagua Nembo', + 'or upload your own' => + 'au kupakia yako mwenyewe', + 'Clear your custom emblem to use a default emblem.' => + 'Futa nembo zako maalum ya kutumia nembo default.', + 'Password is too simple' => + 'Password ni rahisi sana', 'Sign Up' => 'Jiunge', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Muda wa kuanza', 'Expected End Time' => 'Muda tegemewa wa kumaliza', + 'Internationalization' => + 'Umataifishaji', 'Language' => 'Lugha', + 'Branding' => + 'Chapa', + 'Custom Logo' => + 'Desturi za Mkono', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Desturi Nakala', 'DELETE' => 'FUTA', 'Delete' => diff --git a/src/language/lang_tr.php b/src/language/lang_tr.php index 246226e9..5539c3ca 100644 --- a/src/language/lang_tr.php +++ b/src/language/lang_tr.php @@ -85,6 +85,12 @@ 'Şifre', 'Choose an Emblem' => 'Amblem seçin', + 'or upload your own' => + 'veya kendi yüklemenizi yükleyin', + 'Clear your custom emblem to use a default emblem.' => + 'Varsayılan amblemi kullanmak için özel ambleminizi boşaltın.', + 'Password is too simple' => + 'Şifre çok basit', 'Sign Up' => 'Kayıt Olun', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Başlangıç Saati', 'Expected End Time' => 'Tahmini Bitiş Saati', + 'Internationalization' => + 'Uluslararasılaşma', 'Language' => 'Dil', + 'Branding' => + 'Dağlama', + 'Custom Logo' => + 'Özel Logo', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Özel Metin', 'DELETE' => 'SİL', 'Delete' => @@ -453,6 +469,8 @@ 'Oluşturma Saati', 'Last Access' => 'Son Erişim', + 'Last Page Access' => + 'Son Sayfa Erişimi', 'Data' => 'Veri', 'Sessions' => @@ -656,4 +674,12 @@ 'Oyuna geç', 'Powered By Facebook' => 'Facebook Tarafından Geliştiriliyor', + 'Active Directory / LDAP' => + 'Active Directory / LDAP', + 'LDAP Server' => + 'LDAP Sunucusu', + 'LDAP Port' => + 'LDAP Bağlantı Noktası', + 'LDAP Domain' => + 'LDAP Etki Alanı', ); diff --git a/src/language/lang_zh-tw.php b/src/language/lang_zh-tw.php index 75a92dda..2e8f666f 100644 --- a/src/language/lang_zh-tw.php +++ b/src/language/lang_zh-tw.php @@ -85,6 +85,12 @@ '密碼', 'Choose an Emblem' => '選擇一個標誌', + 'or upload your own' => + '或上傳自己的', + 'Clear your custom emblem to use a default emblem.' => + '清除您的自定義會徽以使用默認會徽。', + 'Password is too simple' => + '密碼太簡單了', 'Sign Up' => '註冊', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,16 @@ '開始時間', 'Expected End Time' => '預估結束時間', + 'Internationalization' => + '國際化', 'Language' => '語言', + 'Branding' => + '品牌', + 'Custom Logo' => + '自定義徽標', + 'Custom Text' => + '自定義文本', 'DELETE' => '刪除', 'Delete' => @@ -658,4 +672,12 @@ '跳過教學', 'Powered By Facebook' => 'Powered By Facebook', + 'Active Directory / LDAP' => + 'Active Directory / LDAP', + 'LDAP Server' => + 'LDAP服務器', + 'LDAP Port' => + 'LDAP端口', + 'LDAP Domain' => + 'LDAP域', ); 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/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; } diff --git a/src/models/Control.php b/src/models/Control.php index e6e259d3..25d00b8c 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 { @@ -542,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/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/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 67176dba..0a10fd60 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'], + ); + } } } } @@ -379,6 +383,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 +605,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. @@ -625,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. @@ -637,13 +699,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 +723,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 +742,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 +1012,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 +1167,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 +1281,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 +1334,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 +1342,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/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/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)); diff --git a/src/models/ScoreLog.php b/src/models/ScoreLog.php index 2a68699f..1c534847 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. @@ -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, @@ -217,8 +235,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..2946a8ae 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,8 +358,11 @@ public static function regenerateHash(string $password_hash): bool { $points, $team_id, ); + await Logo::genSetUsed($logo, true); + + Logo::invalidateMCRecords(); 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. @@ -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, @@ -381,7 +404,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); } @@ -415,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, @@ -442,7 +479,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 +501,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 +662,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/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(); 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(); + } } }); } 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()); }