diff --git a/.github/update-composer-json.php b/.github/update-composer-json.php new file mode 100644 index 000000000..2144c54fb --- /dev/null +++ b/.github/update-composer-json.php @@ -0,0 +1,137 @@ + 'package', // The type is at the root level of the repository entry + 'package' => [ + 'name' => $packageName, + 'version' => $packageVersionRange, + 'type' => 'library', // Default type, you can adjust this if needed + 'dist' => [ + 'url' => $repositoryUrl, + 'type' => $distType, // Use the dynamically derived dist type + 'reference' => $packageHash // Adding the hash (reference) if available + ] + ] + ]; + // Track pinned package + $pinnedPackages[] = $packageName; + } +} + +// Write the updated composer.json back to the file +if (file_put_contents($composerJsonPath, json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL)) { + echo "composer.json has been updated with the versions from composer.lock.\n"; + if (count($pinnedPackages) > 0) { + echo "The following packages were pinned to repositories:\n"; + foreach ($pinnedPackages as $packageName) { + echo " - $packageName\n"; + } + } +} else { + echo "Failed to update composer.json.\n"; + exit(1); +} diff --git a/.github/workflows/auto-assign-pr.yml b/.github/workflows/auto-assign-pr.yml new file mode 100644 index 000000000..fecb812bc --- /dev/null +++ b/.github/workflows/auto-assign-pr.yml @@ -0,0 +1,24 @@ +name: Auto-Assign PR + +on: + pull_request_target: + types: [opened] + +permissions: + issues: write + +jobs: + assign: + runs-on: ubuntu-latest + steps: + - name: Assign PR to repo owner + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GH_ADMIN_TOKEN }} + script: | + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + assignees: ["proditis"] + }); diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 7ace1f4be..6a7d02cde 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -3,4 +3,6 @@ MD012: false MD013: false # Disable required empty line after heading MD022: false -MD033: false \ No newline at end of file +MD033: false +MD040: false +MD043: false \ No newline at end of file diff --git a/ansible/Dockerfiles/sanitycheck/variables.yml b/ansible/Dockerfiles/sanitycheck/variables.yml index 9766edd51..1762de0cb 100644 --- a/ansible/Dockerfiles/sanitycheck/variables.yml +++ b/ansible/Dockerfiles/sanitycheck/variables.yml @@ -12,6 +12,7 @@ writeup_allowed: 0 headshot_spin: 0 instance_allowed: 0 TargetOndemand: false +dynamic_treasures: 0 container: name: "{{hostname}}" hostname: "{{fqdn}}" diff --git a/ansible/files/pui.service.conf b/ansible/files/pui.service.conf index fb1fa8ea0..9dd62ce24 100644 --- a/ansible/files/pui.service.conf +++ b/ansible/files/pui.service.conf @@ -1,6 +1,8 @@ # Allow users to connect to port 80/443 pass in quick on egress inet proto tcp from {, } to (egress:0) port 8888 rdr-to 127.0.0.1 +pass in quick on interconnect inet proto tcp from (interconnect:network) to (interconnect:0) port 8888 rdr-to 127.0.0.1 + pass quick from label "administrators" pass quick inet proto tcp from to (egress:0) port { 80 , 443 } label "www-moderators" @@ -8,9 +10,7 @@ pass quick inet proto tcp from to (egress:0) port { 80 , 443 } labe # FOR DT OPERATIONS pass in quick inet proto tcp from to port 80 rdr-to 127.0.0.1 port 8080 label "maintenance" pass in quick inet proto tcp from to port 443 rdr-to 127.0.0.1 port 8443 label "maintenance" -block in quick on egress inet proto tcp from to (egress:0) port 8888 pass in on egress inet proto tcp from to port { 80, 443 } label "www-normal" pass in on egress inet proto tcp to port { 80, 443 } label "www-normal" -pass in quick on egress inet proto tcp to (egress:0) port 8888 rdr-to 127.0.0.1 diff --git a/ansible/inventories/servers/host_vars/registry.yml b/ansible/inventories/servers/host_vars/registry.yml index 87ad4d3cc..37fa85f1e 100644 --- a/ansible/inventories/servers/host_vars/registry.yml +++ b/ansible/inventories/servers/host_vars/registry.yml @@ -5,7 +5,7 @@ registry_storage: "/storage" registry_bind_ip: "0.0.0.0" registry_bind_port: "5000" registry_targets_if: if0 -registry_targets_cidr: 10.0.100.0/24 +registry_targets_cidr: 10.0.0.100/24 backups: - { tgz: "/altroot/root.tgz", src: '/root' } - { tgz: "/altroot/etc.tgz", src: '/etc' } diff --git a/ansible/maintenance/authorized_keys.yml b/ansible/maintenance/authorized_keys.yml new file mode 100644 index 000000000..cda89202b --- /dev/null +++ b/ansible/maintenance/authorized_keys.yml @@ -0,0 +1,33 @@ +--- +- name: Update authorized SSH keys for a user + hosts: all + become: true + + vars: + target_user: root + + pre_tasks: + - name: Ensure ssh_keys_source is provided + ansible.builtin.fail: + msg: "You must provide ssh_keys_source (file path or URL)" + when: ssh_keys_source is not defined + + - name: Load SSH public keys from file or URL + ansible.builtin.set_fact: + ssh_public_keys: >- + {{ + lookup( + ssh_keys_source is match('^https?://') + | ternary('url', 'file'), + ssh_keys_source + ) + }} + + tasks: + - name: Update authorized_keys for user + ansible.posix.authorized_key: + user: "{{ target_user }}" + key: "{{ ssh_public_keys }}" + manage_dir: true + state: present + # exclusive: true # uncomment to replace all existing keys diff --git a/ansible/maintenance/sync-github.yml b/ansible/maintenance/sync-github.yml new file mode 100644 index 000000000..e413658f2 --- /dev/null +++ b/ansible/maintenance/sync-github.yml @@ -0,0 +1,32 @@ +#!/usr/bin/env ansible-playbook +--- +- name: Update deployed application from Git + hosts: all + tasks: + - name: Ensure repository is up to date + git: + repo: "{{ GITHUB_REPO }}" + dest: "{{ REPO }}" + version: "{{ GITHUB_REPO_BRANCH }}" + force: yes + accept_hostkey: yes + update: yes + notify: restart applications + register: git_result + + - name: Clean untracked files (preserving paths) + shell: git clean -fd {{ '-n' if ansible_check_mode else '' }} {% for p in PRESERVE_PATHS %}-e {{ p }} {% endfor %} + args: + chdir: "{{ REPO }}" + register: clean_result + changed_when: clean_result.stdout != "" + when: git_result.changed and PRESERVE_PATHS is defined + notify: restart applications + + handlers: + - name: restart applications + when: APP_SERVICES is defined + service: + name: "{{ item }}" + state: restarted + loop: "{{ APP_SERVICES }}" diff --git a/ansible/runonce/db.yml b/ansible/runonce/db.yml index 3409ab90d..58709d7bd 100755 --- a/ansible/runonce/db.yml +++ b/ansible/runonce/db.yml @@ -411,6 +411,27 @@ group: wheel mode: '0555' + - name: copy watchdoger script + copy: + src: ../../contrib/watchdoger.py + dest: /usr/local/bin/watchdoger + owner: root + group: wheel + mode: '0555' + + - name: Install watchdoger.ini for supervisord + copy: + dest: /etc/supervisord.d/watchdoger.ini + content: | + [program:watchdoger] + user = root + command = /usr/local/bin/watchdoger --file_path /tmp/event_finished --url {{ wsserver.url | default("http://10.7.0.200:8888/broadcast") }} --token {{ wsserver.token | default("server123token") }} + autorestart = false + startretries = 0 + stdout_logfile=/var/log/watchdoger.log + stdout_logfile_maxbytes=0 + redirect_stderr=true + - name: Setting up sysctl.conf sysctl: name: "{{ item.key }}" diff --git a/ansible/runonce/docker-registry.yml b/ansible/runonce/docker-registry.yml index 547330003..49b150e46 100755 --- a/ansible/runonce/docker-registry.yml +++ b/ansible/runonce/docker-registry.yml @@ -137,7 +137,7 @@ content: "{{item.content|default(omit)}}" with_items: - { dest: "/etc/pf.conf", src: "../files/pf.conf"} - - { dest: "/etc/service.pf.conf", content: "anchor \"dynamic\"\npass quick inet proto tcp from to port {{registry_bind_port}} label \"service_clients\"\n"} + - { dest: "/etc/service.pf.conf", content: "pass quick from label \"administrators\"\nanchor \"dynamic\"\npass quick inet proto tcp from to port {{registry_bind_port}} label \"service_clients\"\n"} - { dest: "/etc/service_clients.conf", content: "{{registry_targets_cidr}}\n"} - name: Dump administrators PF table (if exists) diff --git a/ansible/runonce/docker-servers.yml b/ansible/runonce/docker-servers.yml index af2024e10..1b9d3e404 100755 --- a/ansible/runonce/docker-servers.yml +++ b/ansible/runonce/docker-servers.yml @@ -1,40 +1,17 @@ #!/usr/bin/env ansible-playbook --- -- name: Configure docker servers for echoCTF +- name: Bootstrap docker servers for echoCTF hosts: all gather_facts: true become: true become_method: su + remote_user: sysadmin tasks: + - name: Set timezone to UTC timezone: name: UTC - - name: Kill any running pm2 - shell: pm2 kill - no_log: true - ignore_errors: true - - - name: Ensure docker services are stopped - command: service docker stop - no_log: true - ignore_errors: true - - - name: Ensure existing docker service overrides are removed - ansible.builtin.file: - path: "{{item}}" - state: absent - with_items: - - /etc/systemd/system/docker.service.d/dockerd-service-override.conf - - /etc/systemd/system/docker.service.d/override.conf - - /etc/systemd/system/pm2-root.service.d/pm2-root-service-override.conf - - /etc/systemd/system/pm2-root.service.d/override.conf - - - name: Remove any existing /etc/docker/daemon.json - ansible.builtin.file: - path: /etc/docker/daemon.json - state: absent - - name: Set hostname based on host_var hostname: name: "{{fqdn}}" @@ -61,29 +38,6 @@ pkg: "{{pre_apt}}" when: pre_apt is defined and pre_apt|length > 0 - - name: Add apt keys - when: aptKeys is defined - apt_key: - url: "{{item.key}}" - state: "{{item.state}}" - with_items: "{{aptKeys}}" - - - name: Add apt repositories - when: aptRepos is defined - apt_repository: - repo: "{{item.repo}}" - state: "{{item.state}}" - with_items: "{{aptRepos}}" - - - name: Update package cache - apt: - update_cache: yes - - - name: Update all packages to the latest version - no_log: "{{DEBUG|default(true)}}" - apt: - upgrade: dist - - name: Adding defined users (optional) when: users is defined user: @@ -109,16 +63,73 @@ key: "https://github.com/{{item}}.keys" with_items: "{{sshkeys}}" - - name: Make sure sysadmin has sudo access - lineinfile: - path: /etc/sudoers.d/90_sysadmin - line: 'sysadmin ALL=(ALL) NOPASSWD:ALL' - create: yes - - name: Ensure /home/sysadmin is owned by sysadmin user (recursive) shell: chown -R sysadmin /home/sysadmin when: users is defined + - name: Allow user to have passwordless sudo + copy: + dest: "/etc/sudoers.d/90_sysadmin" + content: "sysadmin ALL=(ALL) NOPASSWD:ALL\n" + owner: root + group: root + mode: '0440' + +- name: Configure docker servers for echoCTF + hosts: all + gather_facts: true + become: false + remote_user: root + tasks: + + - name: Kill any running pm2 + shell: pm2 kill + no_log: true + ignore_errors: true + + - name: Ensure docker services are stopped + command: service docker stop + no_log: true + ignore_errors: true + + - name: Ensure existing docker service overrides are removed + ansible.builtin.file: + path: "{{item}}" + state: absent + with_items: + - /etc/systemd/system/docker.service.d/dockerd-service-override.conf + - /etc/systemd/system/docker.service.d/override.conf + - /etc/systemd/system/pm2-root.service.d/pm2-root-service-override.conf + - /etc/systemd/system/pm2-root.service.d/override.conf + + - name: Remove any existing /etc/docker/daemon.json + ansible.builtin.file: + path: /etc/docker/daemon.json + state: absent + + - name: Add apt keys + when: aptKeys is defined + apt_key: + url: "{{item.key}}" + state: "{{item.state}}" + with_items: "{{aptKeys}}" + + - name: Add apt repositories + when: aptRepos is defined + apt_repository: + repo: "{{item.repo}}" + state: "{{item.state}}" + with_items: "{{aptRepos}}" + + - name: Update package cache + apt: + update_cache: yes + + - name: Update all packages to the latest version + no_log: "{{DEBUG|default(true)}}" + apt: + upgrade: dist + - name: Install post install packages apt: state: latest diff --git a/ansible/runonce/mui.yml b/ansible/runonce/mui.yml index b3c247965..4f27b56e5 100755 --- a/ansible/runonce/mui.yml +++ b/ansible/runonce/mui.yml @@ -272,7 +272,7 @@ dest: "{{item.dest}}" with_items: - { src: '{{playbook_dir}}/../templates/httpd.conf.j2', dest: '/etc/httpd.conf', domain: '{{hostname}}' } - - { src: '{{playbook_dir}}/../templates/acme-client.conf.j2', dest: '/etc/acme-client.conf', domain: '{{hostname}}', challenge_dir: "/home/moderatortUI/acme/.well-known/acme-challenge/" } + - { src: '{{playbook_dir}}/../templates/acme-client.conf.j2', dest: '/etc/acme-client.conf', domain: '{{hostname}}', challenge_dir: "/home/moderatorUI/acme/.well-known/acme-challenge/" } - name: Generate pf tables files command: "{{item.cmd}}" @@ -425,8 +425,8 @@ - name: configure folders and perms command: "{{item}}" with_items: - - "mkdir -p /home/moderatorUI/{{domain_name}}/backend/web/assets" - - "chown -R moderatorUI /home/moderatorUI/{{domain_name}}/backend/web/assets" + - "install -d -o moderatorUI /home/moderatorUI/{{domain_name}}/backend/web/assets" + - "install -d -o moderatorUI /home/moderatorUI/{{domain_name}}/backend/web/identifcationFiles" - "mkdir -p /var/log/cron" - "ln -sf /home/moderatorUI/{{domain_name}}/backend/yii /usr/local/bin/backend" diff --git a/ansible/runonce/pui.yml b/ansible/runonce/pui.yml index 9a1ed2bf4..c23f90dd2 100755 --- a/ansible/runonce/pui.yml +++ b/ansible/runonce/pui.yml @@ -417,7 +417,7 @@ src: ../templates/dt.conf.j2 dest: /etc/nginx/dt.conf with_items: - - { user: 'www', domain: '{{domain_name}}', root: "/htdocs" } + - { user: 'participantUI', domain: '{{domain_name}}', root: "/htdocs" } tags: - nginx @@ -466,6 +466,7 @@ - { section: PHP, option: "error_reporting", value: "E_NONE" } - { section: PHP, option: "post_max_size", value: "180M" } - { section: PHP, option: "upload_max_filesize", value: "120M" } + - { section: PHP, option: "allow_url_fopen", value: "On"} - { section: Session, option: "session.save_handler", value: "memcached"} - { section: Session, option: "session.save_path", value: "{{db_ip}}:11211"} - { section: Session, option: "session.gc_maxlifetime", value: "43200" } @@ -487,13 +488,10 @@ - name: configure folders and permissions command: "{{item}}" with_items: - - mkdir -p /home/participantUI/{{domain_name}}/frontend/web/assets - - mkdir -p /home/participantUI/{{domain_name}}/frontend/web/identificationFiles - - mkdir -p /home/participantUI/{{domain_name}}/frontend/web/images/avatars/team + - install -d -o participantUI /home/participantUI/{{domain_name}}/frontend/web/assets + - install -d -o participantUI /home/participantUI/{{domain_name}}/frontend/web/identificationFiles + - install -d -o participantUI /home/participantUI/{{domain_name}}/frontend/web/images/avatars/team - mkdir -p /var/log/cron - - chown -R participantUI /home/participantUI/{{domain_name}}/frontend/web/assets - - chown -R participantUI /home/participantUI/{{domain_name}}/frontend/web/images/avatars/ - - chown -R participantUI /home/participantUI/{{domain_name}}/frontend/web/identificationFiles - ln -sf /home/participantUI/{{domain_name}}/frontend/yii /usr/local/bin/frontend - name: configure participant rc.d @@ -635,10 +633,12 @@ redirect_stderr=true - name: "Update frontend/config/params.php" - replace: - path: "/home/participantUI/{{domain_name}}/frontend/config/params.php" - regexp: 'ws://localhost:8888/ws' - replace: 'wss://{{domain_name}}/ws' + ansible.builtin.lineinfile: + path: "/home/participantUI/{{ domain_name }}/frontend/config/params.php" + search_string: 'ws://localhost:8888/ws' + line: 'wss://{{ domain_name }}/ws' + insertafter: null + insertbefore: null - name: copy nstables script copy: diff --git a/ansible/runonce/vpngw.yml b/ansible/runonce/vpngw.yml index 495bcef33..d2c3902ac 100755 --- a/ansible/runonce/vpngw.yml +++ b/ansible/runonce/vpngw.yml @@ -252,6 +252,19 @@ - name: Activate install php modules shell: "cp /etc/php-{{versions.PHP}}.sample/*.ini /etc/php-{{versions.PHP}}/" + - name: Fixing /etc/php.ini + ini_file: dest=/etc/php-{{versions.PHP}}.ini section={{item.section}} option={{item.option}} value={{item.value}} mode=0644 owner=root group=wheel + with_items: + - { section: PHP, option: "expose_php", value: "Off" } + - { section: PHP, option: "log_errors_max_len", value: 4096 } + - { section: PHP, option: "html_errors", value: "Off" } + - { section: PHP, option: "max_execution_time", value: "60" } + - { section: PHP, option: "max_input_time", value: "120" } + - { section: PHP, option: "memory_limit", value: "256M" } + - { section: PHP, option: "error_reporting", value: "E_NONE" } + - { section: PHP, option: "post_max_size", value: "180M" } + - { section: PHP, option: "upload_max_filesize", value: "120M" } + - { section: PHP, option: "allow_url_fopen", value: "On"} - name: Update my.cnf ini_file: @@ -686,6 +699,28 @@ stdout_logfile_maxbytes=0 redirect_stderr=true + - name: copy watchdog-action script + copy: + src: ../../contrib/watchdog-action.py + dest: /usr/local/bin/watchdog-action + owner: root + group: wheel + mode: '0555' + + - name: copy event_shutdown script + copy: + src: ../../contrib/event_shutdown.sh + dest: /usr/local/sbin/event_shutdown + owner: root + group: wheel + mode: '0555' + + - name: Add watchdog-action to rc.local + lineinfile: + path: "/etc/rc.local" + line: "nohup /usr/local/bin/watchdog-action --file_path /tmp/event_finished --action /usr/local/sbin/event_shutdown >/tmp/watchdog.log 2>&1 &" + insertafter: EOF + - name: create user for openvpn up/down scripts mysql_user: name: "openvpn" diff --git a/ansible/templates/dt.conf.j2 b/ansible/templates/dt.conf.j2 index 17f7650b4..5a3699bdc 100644 --- a/ansible/templates/dt.conf.j2 +++ b/ansible/templates/dt.conf.j2 @@ -43,7 +43,15 @@ http { ssl_prefer_server_ciphers on; ssl_dhparam /etc/ssl/private/dhparam.pem; - return 503; + # ACME challenge must bypass maintenance + location ^~ /.well-known/acme-challenge/ { + root /acme; + try_files $uri =404; + } + + location / { + return 503; + } error_page 503 @maintenance; location @maintenance { add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; diff --git a/ansible/templates/nginx.conf.j2 b/ansible/templates/nginx.conf.j2 index f2584dcc6..30552f110 100644 --- a/ansible/templates/nginx.conf.j2 +++ b/ansible/templates/nginx.conf.j2 @@ -252,6 +252,15 @@ http { set $yii_bootstrap "/index.php"; {% if "mui" not in inventory_hostname %} + {% if mui_ext_ip is defined %} + location ^~ /identificationFiles/ { + allow {{ mui_ext_ip }}; + deny all; + + try_files $uri $uri/ =404; + } + {% endif %} + # Rate limit /api to 10 requests/sec # This is way too loose location /api { @@ -336,18 +345,18 @@ http { return 404; } {% endif %} - location /ws { - proxy_pass http://127.0.0.1:8888/ws; - - # WebSocket-specific headers - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - - # Optional: increase timeout for long-lived connections - proxy_read_timeout 86400s; - proxy_send_timeout 86400s; - } + location /ws { + proxy_pass http://127.0.0.1:8888/ws; + + # WebSocket-specific headers + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + + # Optional: increase timeout for long-lived connections + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } {% if captcha is defined %} # Check for requests to /profile/ location ~ ^/profile/\d+$ { diff --git a/backend/commands/CronController.php b/backend/commands/CronController.php index ec16a5bc0..dda45d3bf 100644 --- a/backend/commands/CronController.php +++ b/backend/commands/CronController.php @@ -15,6 +15,7 @@ use app\modules\infrastructure\models\TargetInstanceAudit; use app\modules\infrastructure\models\NetworkTargetSchedule as NTS; use app\modules\gameplay\models\NetworkTarget; +use app\components\helpers\ArrayHelperExtended; /** * @method docker_connect() @@ -159,11 +160,7 @@ public function actionInstancePf($before = 60) case SELF::ACTION_START: case SELF::ACTION_RESTART: if (($val->team_allowed === true || \Yii::$app->sys->team_visible_instances === true) && $val->player->teamPlayer !== null && $val->player->teamPlayer->approved === 1) { - foreach ($val->player->teamPlayer->team->approvedMembers as $teamPlayer) { - if ((int)$teamPlayer->player->last->vpn_local_address !== 0) { - $ips[] = long2ip($teamPlayer->player->last->vpn_local_address); - } - } + $ips = $val->player->teamPlayer->team->approvedMemberIPs; } else if ((int)$val->player->last->vpn_local_address !== 0) { $ips[] = long2ip($val->player->last->vpn_local_address); } @@ -200,15 +197,9 @@ public function actionInstancePfTables($dopf = false) } $team_visible_instances = \Yii::$app->sys->team_visible_instances; if ($val->team_allowed == true || $team_visible_instances === true) { - // get team members if ($val->player->teamPlayer) { $team = $val->player->teamPlayer->team; - foreach ($team->approvedMembers as $member) { - // get IP's of connected players - if (($pIP = $member->player->last->vpn_local_address) !== null) { - $IPs[] = long2ip($pIP); - } - } + $IPs = ArrayHelperExtended::mergeUnique($IPs, $team->approvedMemberIPs); } } @@ -224,8 +215,16 @@ public function actionInstancePfTables($dopf = false) /** * Process player private instances + * + * This command updates the last updated_at field for active instances + * and processes instances that are pending action (eg. reboot, destroy, powerup) + * and updates the PF according to currently online players. + * + * @param bool $pfonly Perform PF operations and skip instance docker actions (default: false) + * @param int $expired_ago Filter instances based that haven't been updated X seconds ago (default: 2400 seconds = 40 minutes) + * @return int Exit code */ - public function actionInstances($pfonly = false) + public function actionInstances(bool $pfonly = false, int $expired_ago = 2400) { if (file_exists("/tmp/cron-instances.lock")) { echo date("Y-m-d H:i:s ") . "Instances: /tmp/cron-instances.lock exists, skipping execution\n"; @@ -233,21 +232,13 @@ public function actionInstances($pfonly = false) } touch("/tmp/cron-instances.lock"); $action = SELF::ACTION_EXPIRED; - // Get powered instances - $t = TargetInstance::find()->active(); - foreach ($t->all() as $instance) { - if ($instance->player->last->vpn_local_address !== null && $pfonly === false) { - printf("Updating heartbeat [%d: %s for %d: %s]\n", $instance->target_id, $instance->target->name, $instance->player_id, $instance->player->username); - $instance->touch('updated_at'); - } - } - - $t = TargetInstance::find()->pending_action(40); + $t = TargetInstance::find()->pending_action($expired_ago); foreach ($t->all() as $val) { try { $ips = []; $dc = new DockerContainer($val->target); - $dc->timeout = ($val->server->timeout ? $val->server->timeout : 2000); + $dc->timeout = ($val->server->timeout ? $val->server->timeout : 10000); + if ($val->target->targetVolumes !== null) $dc->targetVolumes = $val->target->targetVolumes; @@ -279,17 +270,18 @@ public function actionInstances($pfonly = false) $dc->server = $val->server->connstr; $dc->net = $val->server->network; - if ($val->ip == null) { - echo date("Y-m-d H:i:s ") . "Starting"; - $action = SELF::ACTION_START; + if ($val->reboot === 2) { + echo date("Y-m-d H:i:s ") . "Destroying"; + $action = SELF::ACTION_DESTROY; } else if ($val->reboot === 1) { echo date("Y-m-d H:i:s ") . "Restarting"; $action = SELF::ACTION_RESTART; - } else if ($val->reboot === 2) { - echo date("Y-m-d H:i:s ") . "Destroying"; - $action = SELF::ACTION_DESTROY; + } else if ($val->ip == null && $val->reboot == 0) { + echo date("Y-m-d H:i:s ") . "Starting"; + $action = SELF::ACTION_START; } else { echo date("Y-m-d H:i:s ") . "Expiring"; + $action = SELF::ACTION_EXPIRED; } printf(" %s for %s (%s) at %s\n", $val->target->name, $val->player->username, $dc->name, $val->server->name); switch ($action) { @@ -298,17 +290,14 @@ public function actionInstances($pfonly = false) if ($pfonly === false) { try { $dc->destroy(); - } catch (\Exception $e) { + } catch (\Throwable $e) { } $dc->pull(); + usleep(100); $dc->spin(); } if (($val->team_allowed === true || \Yii::$app->sys->team_visible_instances === true) && $val->player->teamPlayer && $val->player->teamPlayer->approved === 1) { - foreach ($val->player->teamPlayer->team->approvedMembers as $teamPlayer) { - if ((int)$teamPlayer->player->last->vpn_local_address !== 0) { - $ips[] = long2ip($teamPlayer->player->last->vpn_local_address); - } - } + $ips = $val->player->teamPlayer->team->approvedMemberIPs; } else if ((int)$val->player->last->vpn_local_address !== 0) { $ips[] = long2ip($val->player->last->vpn_local_address); } @@ -317,8 +306,10 @@ public function actionInstances($pfonly = false) $val->ipoctet = $dc->container->getNetworkSettings()->getNetworks()->{$val->server->network}->getIPAddress(); Pf::add_table_ip($dc->name, $val->ipoctet, true); $val->reboot = 0; - if ($pfonly === false) + if ($pfonly === false) { + $val->updated_at = new \yii\db\Expression('NOW()'); $val->save(); + } break; case SELF::ACTION_EXPIRED: @@ -337,14 +328,32 @@ public function actionInstances($pfonly = false) default: printf("Error: Unknown action\n"); } - } catch (\Exception $e) { + } catch (\Throwable $e) { if (method_exists($e, 'getErrorResponse')) - echo "Instances:", $e->getErrorResponse()->getMessage(), "\n"; + echo "Instances: ", $e->getErrorResponse()->getMessage(), "\n"; else - echo "Instances:", $e->getMessage(), "\n"; + echo "Instances: ", $e->getMessage(), "\n"; + if (getenv('DEBUG', true) !== false) + \Yii::error($e); } + unset($dc); + usleep(200); + $publisher = new \app\services\ServerPublisher(\Yii::$app->params['serverPublisher']); + $publisher->publish($val->player_id, 'target', ['id' => $val->target_id]); } // end foreach $this->actionInstancePfTables(true); + + if ($pfonly === false) { + try { + $t = TargetInstance::find()->active()->withApprovedMemberHeartbeat()->last_updated(round($expired_ago / 2)); + foreach ($t->all() as $instance) { + printf("Updating heartbeat [%d: %s for %d: %s]\n", $instance->target_id, $instance->target->name, $instance->player_id, $instance->player->username); + $instance->touch('updated_at'); + } + } catch (\Throwable $e) { + echo "Instrances: Error while touching updated_at. ", $e->getMessage(); + } + } @unlink("/tmp/cron-instances.lock"); } diff --git a/backend/commands/PlayerController.php b/backend/commands/PlayerController.php index 586d94869..f01f531df 100644 --- a/backend/commands/PlayerController.php +++ b/backend/commands/PlayerController.php @@ -182,7 +182,7 @@ public function actionMail($active = false, $email = false, $status = 9, $approv /* Register Users and generate OpenVPN keys and settings */ - public function actionRegister($username, $email, $fullname, $password = false, $player_type = "offense", $active = false, $academic = false, $team_name = false, $approved = 0) + public function actionRegister($username, $email, $fullname, $password = false, $player_type = "offense", bool $active = true, int $status = 9, $academic = false, $team_name = false, $approved = 0) { echo "Registering: ", $email, "\n"; $trans = Yii::$app->db->beginTransaction(); @@ -196,7 +196,7 @@ public function actionRegister($username, $email, $fullname, $password = false, $unique = Player::findOne(['username' => $username]); } $player->username = $username; - echo 'Autogenerated username: ',$player->username,"\n"; + echo 'Autogenerated username: ', $player->username, "\n"; } else $player->username = trim(str_replace(array("\xc2\xa0", "\r\n", "\r"), "", $username)); @@ -210,16 +210,16 @@ public function actionRegister($username, $email, $fullname, $password = false, $player->password = Yii::$app->security->generatePasswordHash($password); $player->active = intval($active); - $player->status = 10; + $player->status = $status; $player->auth_key = Yii::$app->security->generateRandomString(); if (!$player->saveWithSsl()) { - if (!$player->active && intval(\Yii::$app->sys->disable_mailer)==0) { - $player->generateEmailVerificationToken(); - $player->status = 9; - } - throw new ConsoleException('Failed to save player:' . $player->username, "\n"); + throw new ConsoleException('Failed to save player:' . $player->username . "\n"); + } + + if ((!$player->active || $player->status === 9) && intval(\Yii::$app->sys->disable_mailer) == 0) { + $player->generateEmailVerificationToken(); } $player->createTeam($team_name, $approved); @@ -366,23 +366,23 @@ public function actionCheckDupips($skip_uids = false) } } - public function actionFailValidation($delete = false,$extended=false,$csv=false) + public function actionFailValidation($delete = false, $extended = false, $csv = false) { $allRecords = Player::find()->where(['status' => 10])->all(); foreach ($allRecords as $p) { - if($extended!==false) + if ($extended !== false) $p->scenario = 'extendedValidator'; else $p->scenario = 'validator'; if (!$p->validate()) { - echo boolval($csv)===true ? $p->id.",".$p->username : Table::widget([ - 'headers' => ['ID: ' . $p->id.' '.$p->username, 'Description'], + echo boolval($csv) === true ? $p->id . "," . $p->username : Table::widget([ + 'headers' => ['ID: ' . $p->id . ' ' . $p->username, 'Description'], 'rows' => $this->getErrorRows($p), ]); if (boolval($delete) !== false && $p->delete()) { - echo boolval($csv)===true ? ",deleted" : $p->id." Deleted\n"; + echo boolval($csv) === true ? ",deleted" : $p->id . " Deleted\n"; } - echo boolval($csv)===true ? "\n" : ""; + echo boolval($csv) === true ? "\n" : ""; } } } @@ -470,8 +470,7 @@ public function actionFetchIdentification($filter = 'inactive', $scheme = 'https foreach ($players->all() as $player) { $baseDir = \Yii::getAlias('@app/web/identificationFiles/'); echo "processing ", $player->username; - if(!$player->metadata) - { + if (!$player->metadata) { echo " no metadata\n"; continue; } @@ -486,13 +485,10 @@ public function actionFetchIdentification($filter = 'inactive', $scheme = 'https curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if($status==200) - { + if ($status == 200) { echo " => grabbing ", $baseDir . $player->metadata->identificationFile, "\n"; file_put_contents($baseDir . $player->metadata->identificationFile, fopen("$scheme://" . Yii::$app->sys->offense_domain . '/identificationFiles/' . $player->metadata->identificationFile, 'r')); - } - else - { + } else { echo ", remote identification file not found\n"; } } diff --git a/backend/commands/TesterController.php b/backend/commands/TesterController.php index 8f7e288e9..77f0c8bba 100644 --- a/backend/commands/TesterController.php +++ b/backend/commands/TesterController.php @@ -23,8 +23,8 @@ public function actionIndex() echo Table::widget([ 'headers' => ['Action', 'Usage', 'Description'], 'rows' => [ - ['Action' => 'tester/mail', 'Usage' => 'tester/mail email@example.com', 'Description' => 'Send a test mail with the current settings'], -// ['Action' => 'docker', 'Usage' => 'tester/docker nginx:latest', 'Description' => 'Test the a given image to make sure it can be deployed on the configured docker servers of the platform.'], + ['Action' => 'tester/mail', 'Usage' => 'tester/mail email@example.com', 'Description' => 'Send a test mail with the current settings'], + ['Action' => 'tester/ws-notify', 'Usage' => 'tester/ws-notify player', 'Description' => 'Send a test websocket notification to the given player by id'], ], ]); } @@ -72,4 +72,17 @@ public function actionMail($to) $this->stderr("❌ Error: " . $e->getMessage() . "\n"); } } + + public function actionWsNotify($player) + { + $player=\app\modules\frontend\models\Player::findOne($player); + $type = "info"; + $title="title"; + $body="body"; + $cc = true; + $archive = true; + $apiOnly = false; + $player->notify($type, $title, $body, $cc, $archive, $apiOnly = false); + } + } diff --git a/backend/components/helpers/ArrayHelperExtended.php b/backend/components/helpers/ArrayHelperExtended.php new file mode 100644 index 000000000..b170f8b48 --- /dev/null +++ b/backend/components/helpers/ArrayHelperExtended.php @@ -0,0 +1,27 @@ +db->createCommand($this->DROP_SQL)->execute(); + } + + public function down() + { + $this->db->createCommand($this->DROP_SQL)->execute(); + } +} \ No newline at end of file diff --git a/backend/modules/frontend/actions/player/MailAction.php b/backend/modules/frontend/actions/player/MailAction.php index e9c5e9901..3b5de091d 100644 --- a/backend/modules/frontend/actions/player/MailAction.php +++ b/backend/modules/frontend/actions/player/MailAction.php @@ -10,6 +10,7 @@ use app\modules\frontend\models\PlayerSsl; use app\modules\frontend\models\PlayerSearch; use app\modules\settings\models\Sysconfig; +use yii\base\UserException; use yii\helpers\Html; class MailAction extends \yii\base\Action @@ -22,27 +23,17 @@ public function run(int $id, $baseURL = "https://echoctf.red/activate/") { // Get innactive players $player = $this->controller->findModel($id); - if ($player->status == 10) - { + if ($player->status == 10) { \Yii::$app->getSession()->setFlash('warning', Yii::t('app', 'Player already active skipping mail.')); - } - elseif ($player->status == 9) - { - if (\Yii::$app->sys->player_require_approval === true && $player->approval > 0 && $player->approval <= 2) - { + } elseif ($player->status == 9) { + if (\Yii::$app->sys->player_require_approval === true && $player->approval > 0 && $player->approval <= 2) { $this->approvalMail($player); - } - elseif (\Yii::$app->sys->player_require_approval === true && $player->approval > 2 && $player->approval <= 4) - { + } elseif (\Yii::$app->sys->player_require_approval === true && $player->approval > 2 && $player->approval <= 4) { $this->rejectionMail($player); - } - elseif (\Yii::$app->sys->player_require_approval !== true) - { + } elseif (\Yii::$app->sys->player_require_approval !== true) { $this->verificationMail($player); } - } - elseif ($player->status == 0) - { + } elseif ($player->status == 0) { $this->rejectionMail($player); } @@ -60,12 +51,11 @@ public function run(int $id, $baseURL = "https://echoctf.red/activate/") private function rejectionMail($player) { try { - $emailtpl=\app\modules\content\models\EmailTemplate::findOne(['name' => 'rejectVerify']); + $emailtpl = \app\modules\content\models\EmailTemplate::findOne(['name' => 'rejectVerify']); $contentHtml = $this->controller->renderPhpContent("?>" . $emailtpl->html, ['user' => $player]); $contentTxt = $this->controller->renderPhpContent("?>" . $emailtpl->txt, ['user' => $player]); - $subject=Yii::t('app', '{event_name} Account rejected', ['event_name' => trim(Yii::$app->sys->event_name)]); - if(!$player->mail($subject,$contentHtml,$contentTxt)) - { + $subject = Yii::t('app', '{event_name} Account rejected', ['event_name' => trim(Yii::$app->sys->event_name)]); + if (!$player->mail($subject, $contentHtml, $contentTxt)) { throw new \Exception('Could not send mail'); } @@ -85,15 +75,18 @@ private function rejectionMail($player) */ private function approvalMail($player) { + if ($player->verification_token == null) { + throw new UserException("The user has no token to mail"); + } + try { $activationURL = sprintf("https://%s/verify-email?token=%s", \Yii::$app->sys->offense_domain, $player->verification_token); - $emailtpl=\app\modules\content\models\EmailTemplate::findOne(['name' => 'emailVerify']); - $contentHtml = $this->controller->renderPhpContent("?>" . $emailtpl->html, ['user' => $player,'verifyLink'=>$activationURL]); - $contentTxt = $this->controller->renderPhpContent("?>" . $emailtpl->txt, ['user' => $player,'verifyLink'=>$activationURL]); - $subject=Yii::t('app', '{event_name} Account approved', ['event_name' => trim(Yii::$app->sys->event_name)]); + $emailtpl = \app\modules\content\models\EmailTemplate::findOne(['name' => 'emailVerify']); + $contentHtml = $this->controller->renderPhpContent("?>" . $emailtpl->html, ['user' => $player, 'verifyLink' => $activationURL]); + $contentTxt = $this->controller->renderPhpContent("?>" . $emailtpl->txt, ['user' => $player, 'verifyLink' => $activationURL]); + $subject = Yii::t('app', '{event_name} Account approved', ['event_name' => trim(Yii::$app->sys->event_name)]); - if(!$player->mail($subject,$contentHtml,$contentTxt)) - { + if (!$player->mail($subject, $contentHtml, $contentTxt)) { return; } @@ -112,15 +105,18 @@ private function approvalMail($player) */ private function verificationMail($player) { + if ($player->verification_token == null) { + throw new UserException("The user has no token to mail"); + } + try { $activationURL = sprintf("https://%s/verify-email?token=%s", \Yii::$app->sys->offense_domain, $player->verification_token); - $emailtpl=\app\modules\content\models\EmailTemplate::findOne(['name' => 'emailVerify']); - $contentHtml = $this->controller->renderPhpContent("?>" . $emailtpl->html, ['user' => $player,'verifyLink'=>$activationURL]); - $contentTxt = $this->controller->renderPhpContent("?>" . $emailtpl->txt, ['user' => $player,'verifyLink'=>$activationURL]); - $subject=Yii::t('app', 'Account registration for {event_name}', ['event_name' => trim(Yii::$app->sys->event_name)]); + $emailtpl = \app\modules\content\models\EmailTemplate::findOne(['name' => 'emailVerify']); + $contentHtml = $this->controller->renderPhpContent("?>" . $emailtpl->html, ['user' => $player, 'verifyLink' => $activationURL]); + $contentTxt = $this->controller->renderPhpContent("?>" . $emailtpl->txt, ['user' => $player, 'verifyLink' => $activationURL]); + $subject = Yii::t('app', 'Account registration for {event_name}', ['event_name' => trim(Yii::$app->sys->event_name)]); - if(!$player->mail($subject,$contentHtml,$contentTxt)) - { + if (!$player->mail($subject, $contentHtml, $contentTxt)) { throw new \Exception('Could not send mail'); } @@ -129,5 +125,4 @@ private function verificationMail($player) \Yii::$app->getSession()->setFlash('error', Yii::t('app', 'Failed to mail player. {exception}', ['exception' => Html::encode($e->getMessage())])); } } - } diff --git a/backend/modules/frontend/controllers/PlayerController.php b/backend/modules/frontend/controllers/PlayerController.php index 2d8686403..0f4ba589a 100644 --- a/backend/modules/frontend/controllers/PlayerController.php +++ b/backend/modules/frontend/controllers/PlayerController.php @@ -32,7 +32,7 @@ public function behaviors() 'class' => \yii\filters\AccessControl::class, 'rules' => [ '00filtered-actions' => [ - 'actions' => ['mail', 'approve', 'reject', 'export'], + 'actions' => ['mail', 'approve', 'reject', 'export', 'notify', 'set-deleted', 'disconnect-vpn', 'generate-ssl', 'ajax-search'], 'allow' => true, 'roles' => ['@'], ] @@ -616,11 +616,13 @@ public function actionMailFiltered() { $searchModel = new PlayerSearch(); $query = $searchModel->search(['PlayerSearch' => Yii::$app->request->post()]); + $query->query->innerJoinWith('emailToken', false); + $query->query->andFilterWhere(['IS NOT', 'emailToken.token', NULL]); $query->query->andFilterWhere([ 'player.approval' => [1, 3], ]); - //$query->pagination = false; - $query->pagination = ['pageSize' => 5]; + $query->pagination = false; + //$query->pagination = ['pageSize' => 5]; if (intval($query->count) === intval(Player::find()->count())) { Yii::$app->session->setFlash('error', Yii::t('app', 'You have attempted to mail all the players.')); return $this->redirect(['index']); @@ -708,7 +710,7 @@ public function actionAjaxSearch($term, $load = false, $active = null, $status = function ($model) { return [ 'id' => $model->id, - 'pid'=>$model->profile->id, + 'pid' => $model->profile->id, 'label' => sprintf("(id: %d / pid: %d) %s <%s>%s", $model->id, $model->profile->id, $model->username, $model->email, $model->status === 10 ? '' : ' (innactive)'), ]; } @@ -748,7 +750,7 @@ public function actionNotify($id) private function notifyLogic($player, $notificationModel, $ovpn = false, $online = false) { - if ($ovpn && $player->playerLast->vpn_local_address===null) + if ($ovpn && $player->playerLast->vpn_local_address === null) return null; if ($online && !boolval($player->online)) return null; diff --git a/backend/modules/frontend/controllers/ProfileController.php b/backend/modules/frontend/controllers/ProfileController.php index f23c4b624..9fba53c01 100644 --- a/backend/modules/frontend/controllers/ProfileController.php +++ b/backend/modules/frontend/controllers/ProfileController.php @@ -32,7 +32,7 @@ public function behaviors() 'class' => \yii\filters\AccessControl::class, 'rules' => [ '00filtered-actions'=>[ - 'actions' => ['view-full','target-progress','score-monthly','headshots','vpn-history','spin-history','notifications'], + 'actions' => ['view-full','target-progress','score-monthly','headshots','vpn-history','spin-history','notifications','stream-lag','vpn-history-duplicates'], 'allow' => true, 'roles' => ['@'], ] diff --git a/backend/modules/frontend/controllers/TeamController.php b/backend/modules/frontend/controllers/TeamController.php index 604253131..39a3f8067 100644 --- a/backend/modules/frontend/controllers/TeamController.php +++ b/backend/modules/frontend/controllers/TeamController.php @@ -24,7 +24,17 @@ class TeamController extends \app\components\BaseController */ public function behaviors() { - return ArrayHelper::merge(parent::behaviors(), [ + return ArrayHelper::merge([ + 'access' => [ + 'class' => \yii\filters\AccessControl::class, + 'rules' => [ + '00filtered-actions' => [ + 'actions' => ['free-player-ajax-search','notify','ajax-search'], + 'allow' => true, + 'roles' => ['@'], + ] + ], + ], 'rules' => [ 'class' => 'yii\filters\AjaxFilter', 'only' => ['free-player-ajax-search'] @@ -36,7 +46,7 @@ public function behaviors() 'repopulate-stream' => ['POST'], ], ], - ]); + ], parent::behaviors()); } /** @@ -187,13 +197,11 @@ public function actionToggleAcademic($id) $trans = Yii::$app->db->beginTransaction(); try { $model->updateAttributes(['academic' => ($model->academic + 1) % \Yii::$app->sys->academic_grouping]); - foreach($model->teamPlayers as $p) - { - $p->updateAttributes(['academic'=>$model->academic]); + foreach ($model->teamPlayers as $p) { + $p->updateAttributes(['academic' => $model->academic]); } $trans->commit(); \Yii::$app->getSession()->setFlash('success', Yii::t('app', 'Team changed academic grouping.')); - } catch (\Exception $e) { $trans->rollBack(); \Yii::$app->getSession()->setFlash('error', Yii::t('app', 'Failed to update academic grouping for team')); diff --git a/backend/modules/frontend/models/Player.php b/backend/modules/frontend/models/Player.php index 97aec5de9..60782458f 100644 --- a/backend/modules/frontend/models/Player.php +++ b/backend/modules/frontend/models/Player.php @@ -54,6 +54,7 @@ public function getAuthKey() public function saveWithSsl() { if (!$this->save()) { + Yii::error($this->getErrorSummary(true)); return false; } @@ -63,6 +64,7 @@ public function saveWithSsl() if ($playerSsl->save()) { return $playerSsl->refresh(); } + Yii::error($playerSsl->getErrorSummary(true)); return false; } @@ -144,6 +146,11 @@ public function createTeam($team_name, $approved) if (!$tp->save()) echo Yii::t('app', "Error saving team player\n"); + $ti = new \app\modules\frontend\models\TeamInvite(['team_id' => $team->id, 'token' => Yii::$app->security->generateRandomString(8)]); + if (!$ti->save()) { + echo Yii::t('app', "Error saving team invite\n"); + Yii::error($ti->getErrorSummary(true)); + } } /** diff --git a/backend/modules/frontend/models/PlayerAR.php b/backend/modules/frontend/models/PlayerAR.php index 878030d40..5675a39f6 100644 --- a/backend/modules/frontend/models/PlayerAR.php +++ b/backend/modules/frontend/models/PlayerAR.php @@ -526,7 +526,22 @@ public function getAuthKey() */ public function getApiToken() { - return $this->hasOne(PlayerToken::class, ['player_id' => 'id'])->andWhere(['type' => 'API']); + return $this->hasOne(PlayerToken::class, ['player_id' => 'id'])->from(['apiToken' => PlayerToken::tableName()])->andWhere(['apiToken.type' => 'API']); } + /** + * @return \yii\db\ActiveQuery + */ + public function getEmailToken() + { + return $this->hasOne(PlayerToken::class, ['player_id' => 'id'])->from(['emailToken' => PlayerToken::tableName()])->andWhere(['emailToken.type' => 'email_verification']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getPasswordResetToken() + { + return $this->hasOne(PlayerToken::class, ['player_id' => 'id'])->from(['passwordResetToken' => PlayerToken::tableName()])->andWhere(['passwordResetToken.type' => 'password_reset']); + } } diff --git a/backend/modules/frontend/models/PlayerSsl.php b/backend/modules/frontend/models/PlayerSsl.php index 6e8bdae84..0cec4d946 100644 --- a/backend/modules/frontend/models/PlayerSsl.php +++ b/backend/modules/frontend/models/PlayerSsl.php @@ -107,7 +107,9 @@ public function generate() { $CAprivkey=array("file://".$tmpCAprivkey, null); file_put_contents($tmpCAprivkey, Yii::$app->sys->{'CA.key'}); file_put_contents($tmpCAcert, Yii::$app->sys->{'CA.crt'}); - $serial=time(); + do { + $serial=time(); + } while (self::findOne(['serial'=>$serial])!=null); $x509=openssl_csr_sign($csr, $CAcert, $CAprivkey, 3650, array('digest_alg'=>'sha256', 'config'=>Yii::getAlias('@appconfig').'/CA.cnf', 'x509_extensions'=>'usr_cert'), $serial); openssl_csr_export($csr, $csrout); openssl_x509_export($x509, $certout, false); diff --git a/backend/modules/frontend/models/Team.php b/backend/modules/frontend/models/Team.php index ce40c889d..10cf9bb84 100644 --- a/backend/modules/frontend/models/Team.php +++ b/backend/modules/frontend/models/Team.php @@ -22,6 +22,7 @@ * @property Player $owner * @property TeamPlayer[] $teamPlayers * @property Player[] $players + * @property string[] $approvedMemberIPs */ class Team extends \yii\db\ActiveRecord { @@ -98,11 +99,12 @@ public function getTeamPlayers() public function getInstances() { - return $this->hasMany(\app\modules\infrastructure\models\TargetInstance::class, ['player_id' => 'player_id']) - ->via('teamPlayers', function($query) { - $query->andWhere(['approved' => 1]); - }); + return $this->hasMany(\app\modules\infrastructure\models\TargetInstance::class, ['player_id' => 'player_id']) + ->via('teamPlayers', function ($query) { + $query->andWhere(['approved' => 1]); + }); } + /** * @return \yii\db\ActiveQuery */ @@ -111,6 +113,22 @@ public function getApprovedMembers() return $this->hasMany(TeamPlayer::class, ['team_id' => 'id'])->andWhere(['approved' => 1]); } + /** + * Get array of all approved members currently online + * + * @return array + */ + public function getApprovedMemberIPs() + { + $ips=[]; + foreach ($this->approvedMembers as $tp) { + if ((int)$tp->player->last->vpn_local_address !== 0) { + $ips[] = long2ip($tp->player->last->vpn_local_address); + } + } + return $ips; + } + /** * @return \yii\db\ActiveQuery */ @@ -197,6 +215,6 @@ public function getInvite() */ public function repopulateStream() { - return \Yii::$app->db->createCommand('CALL repopulate_team_stream(:tid)',[':tid'=>$this->id])->execute(); + return \Yii::$app->db->createCommand('CALL repopulate_team_stream(:tid)', [':tid' => $this->id])->execute(); } } diff --git a/backend/modules/frontend/views/player/index.php b/backend/modules/frontend/views/player/index.php index 8b684d02c..9238f5ae0 100644 --- a/backend/modules/frontend/views/player/index.php +++ b/backend/modules/frontend/views/player/index.php @@ -22,7 +22,7 @@

'btn btn-success']) ?> 'btn btn-info']) ?> - 'btn','style' => 'background: #4040bf; color: white;',]) ?> + 'btn', 'style' => 'background: #4040bf; color: white;',]) ?> 'btn', 'style' => 'background: #4d246f; color: white;', @@ -45,7 +45,7 @@ 'rowOptions' => function ($model, $key, $index, $grid) { // $model is the current data model being rendered // check your condition in the if like `if($model->hasMedicalRecord())` which could be a method of model class which checks for medical records. - $tmpObj = clone ($model); + $tmpObj = clone($model); $tmpObj->scenario = 'validator'; if (!$tmpObj->validate()) { unset($tmpObj); @@ -112,9 +112,12 @@ [ 'attribute' => 'approval', 'filter' => $searchModel::APPROVAL, + 'format' => 'html', 'visible' => Yii::$app->sys->player_require_approval === true, 'value' => function ($model) { - return $model::APPROVAL[$model->approval]; + if ($model->status === 10) + return "(" . $model::APPROVAL[$model->approval] . ")"; + return "" . $model::APPROVAL[$model->approval] . ""; } ], @@ -138,7 +141,7 @@ return false; }, 'disconnect-vpn' => function ($model) { - if ($model->last->vpn_local_address !== null && $model->disconnectQueue===null) return true; + if ($model->last->vpn_local_address !== null && $model->disconnectQueue === null) return true; return false; }, 'view' => function ($model) { @@ -157,7 +160,7 @@ return false; }, 'mail' => function ($model) { - if ($model->status == 10 || $model->approval == 0) return false; + if ($model->status == 10 || $model->status == 0 || $model->approval == 0 || ( $model->emailToken == null && $model->passwordResetToken == null)) return false; return true; }, 'delete' => function ($model) { diff --git a/backend/modules/gameplay/controllers/ChallengeController.php b/backend/modules/gameplay/controllers/ChallengeController.php index 893a293c5..281693787 100644 --- a/backend/modules/gameplay/controllers/ChallengeController.php +++ b/backend/modules/gameplay/controllers/ChallengeController.php @@ -22,167 +22,171 @@ class ChallengeController extends \app\components\BaseController /** * {@inheritdoc} */ - public function behaviors() - { - return ArrayHelper::merge(parent::behaviors(),[]); - } + public function behaviors() + { + return ArrayHelper::merge(parent::behaviors(), [ + 'access' => [ + 'class' => \yii\filters\AccessControl::class, + 'rules' => [ + 'authActions' => [ + 'allow' => true, + 'actions' => ['index', 'view'], + 'roles' => ['@'], + 'matchCallback' => function () { + return \Yii::$app->user->identity->isAdmin; + }, + ], + ], + ], + ]); + } - /** - * Lists all Challenge models. - * @return mixed - */ - public function actionIndex() - { - $searchModel=new ChallengeSearch(); - $dataProvider=$searchModel->search(Yii::$app->request->queryParams); - - return $this->render('index', [ - 'searchModel' => $searchModel, - 'dataProvider' => $dataProvider, - ]); - } + /** + * Lists all Challenge models. + * @return mixed + */ + public function actionIndex() + { + $searchModel = new ChallengeSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); - /** - * Displays a single Challenge model. - * @param integer $id - * @return mixed - * @throws NotFoundHttpException if the model cannot be found - */ - public function actionView($id) - { - $query=Question::find()->joinWith('challenge'); - - $query->select('question.*,(SELECT COUNT(question_id) FROM player_question WHERE question.id=player_question.question_id) as answered'); - // add conditions that should always apply here - - $dataProvider=new ActiveDataProvider([ - 'query' => $query, - ]); - $query->andFilterWhere([ - 'question.challenge_id' => $id, - ]); - - $dataProvider->setSort([ - 'defaultOrder' => ['weight' => SORT_ASC,'id'=>SORT_ASC], - 'attributes' => array_merge( - $dataProvider->getSort()->attributes, - [ - 'challengename' => [ - 'asc' => ['challengename' => SORT_ASC], - 'desc' => ['challengename' => SORT_DESC], - ], - 'answered' => [ - 'asc' => ['answered' => SORT_ASC], - 'desc' => ['answered' => SORT_DESC], - ], - ] - ), - ]); - - return $this->render('view', [ - 'model' => $this->findModel($id), - 'questionProvider'=>$dataProvider, - ]); - } + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); + } - /** - * Creates a new Challenge model. - * If creation is successful, the browser will be redirected to the 'view' page. - * @return mixed - */ - public function actionCreate() - { - $model=new Challenge(); - - if($model->load(Yii::$app->request->post()) && $model->save()) - { - $model->file=UploadedFile::getInstance($model, 'file'); - try - { - if($model->file) - { - if(trim($model->filename)==='') - { - $model->filename=$model->id; - $model->updateAttributes(['filename'=>$model->id]); - } - $model->file->saveAs(Yii::getAlias(Yii::$app->sys->challenge_home).'/'.$model->filename); - } - Yii::$app->session->addFlash('success', Yii::t('app','Challenge [{name}] created',['name'=>Html::encode($model->name)])); - Yii::$app->session->addFlash('warning', Yii::t('app','Don\'t forget to create a question for the challenge.')); - } - catch(\Exception $e) - { - Yii::$app->session->setFlash('error', Yii::t('app','Failed to create challenge [{name}]',['name'=>Html::encode($model->name)])); - } - return $this->redirect(['view', 'id' => $model->id]); - } + /** + * Displays a single Challenge model. + * @param integer $id + * @return mixed + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionView($id) + { + $query = Question::find()->joinWith('challenge'); + + $query->select('question.*,(SELECT COUNT(question_id) FROM player_question WHERE question.id=player_question.question_id) as answered'); + // add conditions that should always apply here + + $dataProvider = new ActiveDataProvider([ + 'query' => $query, + ]); + $query->andFilterWhere([ + 'question.challenge_id' => $id, + ]); + + $dataProvider->setSort([ + 'defaultOrder' => ['weight' => SORT_ASC, 'id' => SORT_ASC], + 'attributes' => array_merge( + $dataProvider->getSort()->attributes, + [ + 'challengename' => [ + 'asc' => ['challengename' => SORT_ASC], + 'desc' => ['challengename' => SORT_DESC], + ], + 'answered' => [ + 'asc' => ['answered' => SORT_ASC], + 'desc' => ['answered' => SORT_DESC], + ], + ] + ), + ]); + + return $this->render('view', [ + 'model' => $this->findModel($id), + 'questionProvider' => $dataProvider, + ]); + } - return $this->render('create', [ - 'model' => $model, - ]); + /** + * Creates a new Challenge model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ + public function actionCreate() + { + $model = new Challenge(); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + $model->file = UploadedFile::getInstance($model, 'file'); + try { + if ($model->file) { + if (trim($model->filename) === '') { + $model->filename = $model->id; + $model->updateAttributes(['filename' => $model->id]); + } + $model->file->saveAs(Yii::getAlias(Yii::$app->sys->challenge_home) . '/' . $model->filename); + } + Yii::$app->session->addFlash('success', Yii::t('app', 'Challenge [{name}] created', ['name' => Html::encode($model->name)])); + Yii::$app->session->addFlash('warning', Yii::t('app', 'Don\'t forget to create a question for the challenge.')); + } catch (\Exception $e) { + Yii::$app->session->setFlash('error', Yii::t('app', 'Failed to create challenge [{name}]', ['name' => Html::encode($model->name)])); + } + return $this->redirect(['view', 'id' => $model->id]); } - /** - * Updates an existing Challenge model. - * If update is successful, the browser will be redirected to the 'view' page. - * @param integer $id - * @return mixed - * @throws NotFoundHttpException if the model cannot be found - */ - public function actionUpdate($id) - { - $model=$this->findModel($id); - - if($model->load(Yii::$app->request->post()) && $model->save()) - { - $model->file=UploadedFile::getInstance($model, 'file'); - if($model->file !== null) - { - if(trim($model->filename)==='') - { - $model->filename=$model->id; - $model->updateAttributes(['filename'=>$model->id]); - } - $model->file->saveAs(Yii::getAlias(Yii::$app->sys->challenge_home).'/'.$model->filename); - } - Yii::$app->session->addFlash('success', Yii::t('app','Challenge [{name}] updated',['name'=>Html::encode($model->name)])); - return $this->redirect(['view', 'id' => $model->id]); - } + return $this->render('create', [ + 'model' => $model, + ]); + } - return $this->render('update', [ - 'model' => $model, - ]); + /** + * Updates an existing Challenge model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param integer $id + * @return mixed + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionUpdate($id) + { + $model = $this->findModel($id); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + $model->file = UploadedFile::getInstance($model, 'file'); + if ($model->file !== null) { + if (trim($model->filename) === '') { + $model->filename = $model->id; + $model->updateAttributes(['filename' => $model->id]); + } + $model->file->saveAs(Yii::getAlias(Yii::$app->sys->challenge_home) . '/' . $model->filename); + } + Yii::$app->session->addFlash('success', Yii::t('app', 'Challenge [{name}] updated', ['name' => Html::encode($model->name)])); + return $this->redirect(['view', 'id' => $model->id]); } - /** - * Deletes an existing Challenge model. - * If deletion is successful, the browser will be redirected to the 'index' page. - * @param integer $id - * @return mixed - * @throws NotFoundHttpException if the model cannot be found - */ - public function actionDelete($id) - { - $this->findModel($id)->delete(); - - return $this->redirect(['index']); - } + return $this->render('update', [ + 'model' => $model, + ]); + } - /** - * Finds the Challenge model based on its primary key value. - * If the model is not found, a 404 HTTP exception will be thrown. - * @param integer $id - * @return Challenge the loaded model - * @throws NotFoundHttpException if the model cannot be found - */ - protected function findModel($id) - { - if(($model=Challenge::findOne($id)) !== null) - { - return $model; - } + /** + * Deletes an existing Challenge model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param integer $id + * @return mixed + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionDelete($id) + { + $this->findModel($id)->delete(); + + return $this->redirect(['index']); + } - throw new NotFoundHttpException(Yii::t('app','The requested page does not exist.')); + /** + * Finds the Challenge model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param integer $id + * @return Challenge the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($id) + { + if (($model = Challenge::findOne($id)) !== null) { + return $model; } + + throw new NotFoundHttpException(Yii::t('app', 'The requested page does not exist.')); + } } diff --git a/backend/modules/gameplay/controllers/QuestionController.php b/backend/modules/gameplay/controllers/QuestionController.php index d02804c92..83a49e414 100644 --- a/backend/modules/gameplay/controllers/QuestionController.php +++ b/backend/modules/gameplay/controllers/QuestionController.php @@ -17,113 +17,124 @@ class QuestionController extends \app\components\BaseController /** * {@inheritdoc} */ - public function behaviors() - { - return ArrayHelper::merge(parent::behaviors(),[]); - } + public function behaviors() + { + return ArrayHelper::merge(parent::behaviors(), [ + 'access' => [ + 'class' => \yii\filters\AccessControl::class, + 'rules' => [ + 'authActions' => [ + 'allow' => true, + 'actions' => ['index', 'view'], + 'roles' => ['@'], + 'matchCallback' => function () { + return \Yii::$app->user->identity->isAdmin; + }, + ], + ], + ], + + ]); + } - /** - * Lists all Question models. - * @return mixed - */ - public function actionIndex() - { - $searchModel=new QuestionSearch(); - $dataProvider=$searchModel->search(Yii::$app->request->queryParams); - - return $this->render('index', [ - 'searchModel' => $searchModel, - 'dataProvider' => $dataProvider, - ]); - } + /** + * Lists all Question models. + * @return mixed + */ + public function actionIndex() + { + $searchModel = new QuestionSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); - /** - * Displays a single Question model. - * @param integer $id - * @return mixed - * @throws NotFoundHttpException if the model cannot be found - */ - public function actionView($id) - { - return $this->render('view', [ - 'model' => $this->findModel($id), - ]); - } + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); + } - /** - * Creates a new Question model. - * If creation is successful, the browser will be redirected to the 'view' page. - * @return mixed - */ - public function actionCreate() - { - $model=new Question(); - if(\app\modules\gameplay\models\Challenge::find()->count() == 0) - { - // If there are no player redirect to create player page - Yii::$app->session->setFlash('warning', Yii::t('app',"No Challenges found create one first.")); - return $this->redirect(['/frontend/challenge/create']); - } - - if($model->load(Yii::$app->request->post()) && $model->save()) - { - return $this->redirect(['view', 'id' => $model->id]); - } - - return $this->render('create', [ - 'model' => $model, - ]); + /** + * Displays a single Question model. + * @param integer $id + * @return mixed + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionView($id) + { + return $this->render('view', [ + 'model' => $this->findModel($id), + ]); + } + + /** + * Creates a new Question model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ + public function actionCreate() + { + $model = new Question(); + if (\app\modules\gameplay\models\Challenge::find()->count() == 0) { + // If there are no player redirect to create player page + Yii::$app->session->setFlash('warning', Yii::t('app', "No Challenges found create one first.")); + return $this->redirect(['/frontend/challenge/create']); } - /** - * Updates an existing Question model. - * If update is successful, the browser will be redirected to the 'view' page. - * @param integer $id - * @return mixed - * @throws NotFoundHttpException if the model cannot be found - */ - public function actionUpdate($id) - { - $model=$this->findModel($id); - - if($model->load(Yii::$app->request->post()) && $model->save()) - { - return $this->redirect(['view', 'id' => $model->id]); - } - - return $this->render('update', [ - 'model' => $model, - ]); + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->id]); } - /** - * Deletes an existing Question model. - * If deletion is successful, the browser will be redirected to the 'index' page. - * @param integer $id - * @return mixed - * @throws NotFoundHttpException if the model cannot be found - */ - public function actionDelete($id) - { - $this->findModel($id)->delete(); - - return $this->redirect(['index']); + return $this->render('create', [ + 'model' => $model, + ]); + } + + /** + * Updates an existing Question model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param integer $id + * @return mixed + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionUpdate($id) + { + $model = $this->findModel($id); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->id]); } - /** - * Finds the Question model based on its primary key value. - * If the model is not found, a 404 HTTP exception will be thrown. - * @param integer $id - * @return Question the loaded model - * @throws NotFoundHttpException if the model cannot be found - */ - protected function findModel($id) - { - if(($model=Question::findOne($id)) !== null) - { - return $model; - } - - throw new NotFoundHttpException(Yii::t('app','The requested page does not exist.')); + return $this->render('update', [ + 'model' => $model, + ]); + } + + /** + * Deletes an existing Question model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param integer $id + * @return mixed + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionDelete($id) + { + $this->findModel($id)->delete(); + + return $this->redirect(['index']); + } + + /** + * Finds the Question model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param integer $id + * @return Question the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($id) + { + if (($model = Question::findOne($id)) !== null) { + return $model; } + + throw new NotFoundHttpException(Yii::t('app', 'The requested page does not exist.')); + } } diff --git a/backend/modules/gameplay/controllers/TreasureController.php b/backend/modules/gameplay/controllers/TreasureController.php index 8b25e255a..9937f722e 100644 --- a/backend/modules/gameplay/controllers/TreasureController.php +++ b/backend/modules/gameplay/controllers/TreasureController.php @@ -19,7 +19,21 @@ class TreasureController extends \app\components\BaseController */ public function behaviors() { - return ArrayHelper::merge(parent::behaviors(), []); + return ArrayHelper::merge(parent::behaviors(), [ + 'access' => [ + 'class' => \yii\filters\AccessControl::class, + 'rules' => [ + 'authActions' => [ + 'allow' => true, + 'actions' => ['index', 'view'], + 'roles' => ['@'], + 'matchCallback' => function () { + return \Yii::$app->user->identity->isAdmin; + }, + ], + ], + ], + ]); } /** @@ -47,7 +61,7 @@ public function actionValidate() if ($string !== "") { $secretKey = Yii::$app->sys->treasure_secret_key; $results = Yii::$app->db->createCommand("select treasure.id,player.id as player_id from treasure,player where md5(HEX(AES_ENCRYPT(CONCAT(code, player.id), :secretKey))) LIKE :code", [':secretKey' => $secretKey, ':code' => $string])->queryOne(); - if ($results === [] || $results===false) { + if ($results === [] || $results === false) { Yii::$app->session->setFlash('warning', Yii::t('app', "Code not found.")); } else { $player = \app\modules\frontend\models\Player::findOne($results['player_id']); @@ -56,21 +70,18 @@ public function actionValidate() 'username' => $player->username, 'actions' => false ]); - if($player->teamPlayer!==NULL) - { - $msg = sprintf('Code belongs to player [%s] from team [%s] for target %s and treasure %s', $profileLink,$player->teamPlayer->team->name, $treasure->target->name, $treasure->name); - } - else - { + if ($player->teamPlayer !== NULL) { + $msg = sprintf('Code belongs to player [%s] from team [%s] for target %s and treasure %s', $profileLink, $player->teamPlayer->team->name, $treasure->target->name, $treasure->name); + } else { $msg = sprintf('Code belongs to player [%s] for target %s and treasure %s', $profileLink, $treasure->target->name, $treasure->name); } Yii::$app->session->setFlash('success', $msg); - $string=''; + $string = ''; } } - return $this->render('validate',['code'=>$string]); + return $this->render('validate', ['code' => $string]); } /** diff --git a/backend/modules/infrastructure/controllers/TargetMetadataController.php b/backend/modules/infrastructure/controllers/TargetMetadataController.php index 25f062ab9..a7ee6c011 100644 --- a/backend/modules/infrastructure/controllers/TargetMetadataController.php +++ b/backend/modules/infrastructure/controllers/TargetMetadataController.php @@ -19,102 +19,116 @@ class TargetMetadataController extends \app\components\BaseController */ public function behaviors() { - return ArrayHelper::merge(parent::behaviors(), []); + return ArrayHelper::merge(parent::behaviors(), [ + 'access' => [ + 'class' => \yii\filters\AccessControl::class, + 'rules' => [ + 'authActions' => [ + 'allow' => true, + 'actions' => ['index', 'view'], + 'roles' => ['@'], + 'matchCallback' => function () { + return \Yii::$app->user->identity->isAdmin; + }, + ], + ], + ], + ]); } - /** - * Lists all TargetMetadata models. - * @return mixed - */ + /** + * Lists all TargetMetadata models. + * @return mixed + */ public function actionIndex() { - $searchModel = new TargetMetadataSearch(); - $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + $searchModel = new TargetMetadataSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); - return $this->render('index', [ - 'searchModel' => $searchModel, - 'dataProvider' => $dataProvider, - ]); + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); } - /** - * Displays a single TargetMetadata model. - * @param integer $id - * @return mixed - * @throws NotFoundHttpException if the model cannot be found - */ + /** + * Displays a single TargetMetadata model. + * @param integer $id + * @return mixed + * @throws NotFoundHttpException if the model cannot be found + */ public function actionView($id) { - return $this->render('view', [ - 'model' => $this->findModel($id), - ]); + return $this->render('view', [ + 'model' => $this->findModel($id), + ]); } - /** - * Creates a new TargetMetadata model. - * If creation is successful, the browser will be redirected to the 'view' page. - * @return mixed - */ + /** + * Creates a new TargetMetadata model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ public function actionCreate() { - $model = new TargetMetadata(); + $model = new TargetMetadata(); if ($model->load(Yii::$app->request->post()) && $model->save()) { - return $this->redirect(['view', 'id' => $model->target_id]); + return $this->redirect(['view', 'id' => $model->target_id]); } - return $this->render('create', [ - 'model' => $model, - ]); + return $this->render('create', [ + 'model' => $model, + ]); } - /** - * Updates an existing TargetMetadata model. - * If update is successful, the browser will be redirected to the 'view' page. - * @param integer $id - * @return mixed - * @throws NotFoundHttpException if the model cannot be found - */ + /** + * Updates an existing TargetMetadata model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param integer $id + * @return mixed + * @throws NotFoundHttpException if the model cannot be found + */ public function actionUpdate($id) { - $model = $this->findModel($id); + $model = $this->findModel($id); if ($model->load(Yii::$app->request->post()) && $model->save()) { - return $this->redirect(['view', 'id' => $model->target_id]); + return $this->redirect(['view', 'id' => $model->target_id]); } - return $this->render('update', [ - 'model' => $model, - ]); + return $this->render('update', [ + 'model' => $model, + ]); } - /** - * Deletes an existing TargetMetadata model. - * If deletion is successful, the browser will be redirected to the 'index' page. - * @param integer $id - * @return mixed - * @throws NotFoundHttpException if the model cannot be found - */ + /** + * Deletes an existing TargetMetadata model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param integer $id + * @return mixed + * @throws NotFoundHttpException if the model cannot be found + */ public function actionDelete($id) { - $this->findModel($id)->delete(); + $this->findModel($id)->delete(); - return $this->redirect(['index']); + return $this->redirect(['index']); } - /** - * Finds the TargetMetadata model based on its primary key value. - * If the model is not found, a 404 HTTP exception will be thrown. - * @param integer $id - * @return TargetMetadata the loaded model - * @throws NotFoundHttpException if the model cannot be found - */ + /** + * Finds the TargetMetadata model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param integer $id + * @return TargetMetadata the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ protected function findModel($id) { if (($model = TargetMetadata::findOne($id)) !== null) { - return $model; + return $model; } - throw new NotFoundHttpException('The requested page does not exist.'); + throw new NotFoundHttpException('The requested page does not exist.'); } } diff --git a/backend/modules/infrastructure/models/TargetInstanceQuery.php b/backend/modules/infrastructure/models/TargetInstanceQuery.php index b2551b250..da1067901 100644 --- a/backend/modules/infrastructure/models/TargetInstanceQuery.php +++ b/backend/modules/infrastructure/models/TargetInstanceQuery.php @@ -9,14 +9,50 @@ */ class TargetInstanceQuery extends \yii\db\ActiveQuery { + /** + * Filters TargetInstances to those that are active + * and whose team has at least one approved member with vpn_local_address != 0 + */ + public function withApprovedMemberHeartbeat() + { + return $this + // join to the team_instance player with teamPlayer + ->innerJoin(['tp' => 'team_player'], 'target_instance.player_id = tp.player_id AND tp.approved = 1') + // join to the team + ->innerJoin(['t' => 'team'], 'tp.team_id = t.id') + // join to approved members of the team + ->innerJoin(['am' => 'team_player'], 'am.team_id = t.id AND am.approved = 1') + // join approved member's last + ->innerJoin(['al' => 'player_last'], 'al.id = am.player_id and al.vpn_local_address is not null') + // ensure the approved member has a vpn_local_address + ->distinct(); + } + public function active() { - return $this->andWhere('[[ip]] IS NOT NULL')->andWhere('[[reboot]]!=2'); + return $this->andWhere('target_instance.[[ip]] IS NOT NULL')->andWhere('target_instance.[[reboot]]!=2'); + } + + public function last_updated(int $seconds_ago = 1) + { + return $this->andWhere(['<', 'target_instance.[[updated_at]]', new \yii\db\Expression("NOW() - INTERVAL $seconds_ago SECOND")]); } - public function pending_action($minutes_ago = 60) + public function pending_action(int $seconds_ago = 1) { - return $this->andWhere('[[ip]] IS NULL')->orWhere(['>', 'reboot', 0])->orWhere(['<', 'updated_at', new \yii\db\Expression("NOW() - INTERVAL $minutes_ago MINUTE")]); + return $this->addSelect([ + 'target_instance.*', + 'reboot' => new \yii\db\Expression( + "IF(target_instance.updated_at < (NOW() - INTERVAL :seconds SECOND), 2, target_instance.reboot)", + [':seconds' => $seconds_ago] + ), + ]) + ->andWhere([ + 'or', + ['target_instance.ip' => null], + ['>', 'target_instance.reboot', 0], + ['<', 'target_instance.updated_at', new \yii\db\Expression("NOW() - INTERVAL $seconds_ago SECOND")] + ]); } diff --git a/backend/modules/infrastructure/views/target-instance/view.php b/backend/modules/infrastructure/views/target-instance/view.php index 1a6c5f3ae..92ebc9210 100644 --- a/backend/modules/infrastructure/views/target-instance/view.php +++ b/backend/modules/infrastructure/views/target-instance/view.php @@ -66,7 +66,7 @@ 'team_allowed:boolean', [ 'label' => 'encrypted flags', - 'visible'=>$model->target->dynamic_treasures, + 'visible'=>$model->target->dynamic_treasures && \Yii::$app->user->identity->isAdmin, 'format' => 'raw', 'value' => function ($model) { $lines=[]; diff --git a/backend/modules/settings/models/ConfigureForm.php b/backend/modules/settings/models/ConfigureForm.php index 8eb273406..58c6ea1f0 100644 --- a/backend/modules/settings/models/ConfigureForm.php +++ b/backend/modules/settings/models/ConfigureForm.php @@ -57,7 +57,7 @@ class ConfigureForm extends Model public $leaderboard_show_zero; public $time_zone; public $target_days_new = 2; - public $target_days_updated = 1; + public $target_days_updated = 0; public $discord_news_webhook; public $pf_state_limits; public $stripe_apiKey; @@ -472,7 +472,7 @@ public function rules() [['online_timeout'], 'default', 'value' => 900], [['spins_per_day'], 'default', 'value' => 2], ['target_days_new', 'default', 'value' => 1], - ['target_days_updated', 'default', 'value' => 2], + ['target_days_updated', 'default', 'value' => 0], [['event_start', 'event_end', 'registrations_start', 'registrations_end'], 'datetime', 'format' => 'php:Y-m-d H:i:s'], [[ 'dashboard_is_home', diff --git a/backend/modules/settings/models/Sysconfig.php b/backend/modules/settings/models/Sysconfig.php index 021aad19f..308121e49 100644 --- a/backend/modules/settings/models/Sysconfig.php +++ b/backend/modules/settings/models/Sysconfig.php @@ -56,7 +56,7 @@ public function afterFind() if ($this->val == 0 || $this->val == "") $this->val = ""; else - $this->val = Yii::$app->formatter->asDatetime($this->val, 'php:Y-m-d H:i:s', 'UTC'); + $this->val = date('Y-m-d H:i:s', $this->val); break; default: break; @@ -70,7 +70,7 @@ public function beforeSave($insert) $Q = sprintf("DROP EVENT IF EXISTS event_end_notification"); \Yii::$app->db->createCommand($Q)->execute(); if (!empty($this->val)) { - $Q = sprintf("CREATE EVENT event_end_notification ON SCHEDULE AT '%s' DO INSERT INTO `notification`(player_id,category,title,body,archived) SELECT id,'swal:info',memc_get('sysconfig:event_end_notification_title'),memc_get('sysconfig:event_end_notification_body'),0 FROM player WHERE status=10", $this->val); + $Q = sprintf("CREATE EVENT event_end_notification ON SCHEDULE AT '%s' DO BEGIN INSERT INTO `notification`(player_id,category,title,body,archived) SELECT id,'swal:info',memc_get('sysconfig:event_end_notification_title'),memc_get('sysconfig:event_end_notification_body'),0 FROM player WHERE status=10; DO memc_set('event_finished',1); SELECT 1 INTO OUTFILE '/tmp/event_finished';END", $this->val); \Yii::$app->db->createCommand($Q)->execute(); $this->val = strtotime($this->val); } else { @@ -101,11 +101,9 @@ public function afterSave($insert, $changedAttributes) if ($this->id === 'stripe_webhookLocalEndpoint' && array_key_exists('val', $changedAttributes)) { $oldVal = $changedAttributes['val']; $newVal = $this->val; - if(($u=UrlRoute::findOne(['destination'=>'subscription/default/webhook']))!==NULL) - { - $u->updateAttributes(['source'=>$newVal]); + if (($u = UrlRoute::findOne(['destination' => 'subscription/default/webhook'])) !== NULL) { + $u->updateAttributes(['source' => $newVal]); } - } } diff --git a/contrib/event_shutdown.sh b/contrib/event_shutdown.sh new file mode 100644 index 000000000..6217f0f72 --- /dev/null +++ b/contrib/event_shutdown.sh @@ -0,0 +1,6 @@ +#!/bin/ksh +PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/X11R6/bin:/usr/local/sbin:/usr/local/bin +rcctl stop openvpn findingsd heartbeatd inetd cron +supervisorctl stop all +backend target/destroy-instances +ifconfig tun0 down diff --git a/contrib/findingsd-federated.sql b/contrib/findingsd-federated.sql index ac873b622..848499eae 100644 --- a/contrib/findingsd-federated.sql +++ b/contrib/findingsd-federated.sql @@ -116,6 +116,32 @@ CREATE TABLE `player_ssl` ( UNIQUE KEY `serial` (`serial`) ) ENGINE=FEDERATED DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci CONNECTION='mysql://{{db_user}}:{{db_pass}}@{{db_host}}:3306/{{db_name}}/player_ssl'; +DROP TABLE IF EXISTS `private_network`; +CREATE TABLE `private_network` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `player_id` int(11) unsigned DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `team_accessible` tinyint(1) DEFAULT NULL, + `created_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx-private_network-player_id` (`player_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci CONNECTION='mysql://{{db_user}}:{{db_pass}}@{{db_host}}:3306/{{db_name}}/private_network'; + +DROP TABLE IF EXISTS `private_network_target`; +CREATE TABLE `private_network_target` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `private_network_id` int(11) DEFAULT NULL, + `target_id` int(11) NOT NULL, + `ip` int(11) unsigned DEFAULT NULL, + `state` smallint(6) unsigned DEFAULT 0, + `server_id` int(11) DEFAULT NULL, + `ipoctet` varchar(15) GENERATED ALWAYS AS (inet_ntoa(`ip`)) VIRTUAL, + PRIMARY KEY (`id`), + UNIQUE KEY `idx-unique-private_network_id-target_id` (`private_network_id`,`target_id`), + KEY `idx-private_network_target-private_network_id` (`private_network_id`), + KEY `idx-private_network_target-server_id` (`server_id`), + KEY `idx-private_network_target-target_id` (`target_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci CONNECTION='mysql://{{db_user}}:{{db_pass}}@{{db_host}}:3306/{{db_name}}/private_network_target'; DROP TABLE IF EXISTS `debuglogs`; CREATE TABLE debuglogs ( @@ -241,3 +267,17 @@ BEGIN END IF; END // + + +DROP EVENT IF EXISTS `event_shutdown` // +CREATE EVENT `event_shutdown` ON SCHEDULE EVERY 5 SECOND STARTS '2020-01-01 00:00:00' ON COMPLETION PRESERVE ENABLE DO +BEGIN + IF (select memc_server_count()<1) THEN + select memc_servers_set('{{db_host}}:{{memc_port|default(11211)}}') INTO @memc_server_set_status; + END IF; + + IF memc_get('event_finished') IS NOT NULL THEN + ALTER EVENT `event_shutdown` DISABLE; + SELECT 1 INTO OUTFILE '/tmp/event_finished'; + END IF; +END // diff --git a/contrib/sample-migrations/m000000_000001_system_settings.php b/contrib/sample-migrations/m000000_000001_system_settings.php index 5b76f0a90..ff7323611 100644 --- a/contrib/sample-migrations/m000000_000001_system_settings.php +++ b/contrib/sample-migrations/m000000_000001_system_settings.php @@ -31,6 +31,10 @@ class m000000_000001_system_settings extends Migration ['id' => "leaderboard_show_zero", 'val' => "0"], ['id' => "leaderboard_visible_after_event_end", 'val' => "1"], ['id' => "leaderboard_visible_before_event_start", 'val' => "0"], + ['id' => "country_rankings", 'val' => "0"], + ['id' => "player_point_rankings", 'val' => "0"], + ['id' => "player_monthly_rankings", 'val' => "0"], + ['id' => 'frontpage_scenario', 'val' => 'Welcome to our lovely event... Edit from backend Content => Frontpage Scenario'], ['id' => "event_end_notification_title", 'val' => "🎉 Our awesome echoCTF finished 🎉"], ['id' => "event_end_notification_body", 'val' => "The awesome echoCTF is over 🎉🎉🎉 Congratulations to you and your team 👏👏👏 Thank you for participating!!!"], @@ -59,23 +63,38 @@ class m000000_000001_system_settings extends Migration ['id' => "team_manage_members", 'val' => "1"], ['id' => "team_required", 'val' => "1"], ['id' => 'team_visible_instances', 'val' => "1"], + ['id' => 'team_only_leaderboards', 'val' => "1"], + ['id' => 'team_encrypted_claims_allowed', 'val' => "1"], + /** * Player settings */ ['id' => "approved_avatar", 'val' => "1"], ['id' => "player_profile", 'val' => "1"], ['id' => "profile_visibility", 'val' => "public"], - ['id' => "require_activation", 'val' => "0"], - ['id' => 'player_require_identification', 'val' => "0"], + ['id' => "require_activation", 'val' => "1"], + ['id' => 'player_require_identification', 'val' => "1"], ['id' => 'all_players_vip', 'val' => "1"], - ['id' => 'player_require_approval', 'val' => "0"], + ['id' => 'player_require_approval', 'val' => "1"], ['id' => 'profile_discord', 'val' => "1"], ['id' => 'profile_echoctf', 'val' => "1"], ['id' => 'profile_github', 'val' => "1"], ['id' => 'profile_settings_fields', 'val' => 'avatar,bio,country,discord,echoctf,email,fullname,github,pending_progress,twitter,username,visibility'], + ['id' => 'avatar_robohash_set', 'val' => 'set3'], + /** * Configuration settings */ + ['id' => 'target_guest_view_deny', 'val' => '1'], + ['id' => 'disable_ondemand_operations', 'val' => '1'], + ['id' => 'module_smartcity_disabled', 'val' => '1'], + ['id' => 'module_speedprogramming_enabled', 'val' => '0'], + ['id' => 'dashboard_news_total_pages', 'val' => '10'], + ['id' => 'dashboard_news_records_per_page', 'val' => '3'], + ['id' => 'force_https_urls', 'val' => '1'], + ['id' => 'subscriptions_menu_show', 'val' => '0'], + ['id' => 'log_failed_claims', 'val' => '1'], + ['id' => 'academic_grouping', 'val' => '0'], ['id' => "challenge_home", 'val' => "uploads/"], ['id' => "dashboard_is_home", 'val' => "1"], @@ -126,8 +145,11 @@ class m000000_000001_system_settings extends Migration */ public function safeUp() { - foreach ($this->news as $entry) + foreach ($this->news as $entry) { + $entry['created_at']=new \yii\db\Expression('NOW()'); + $entry['updated_at']=new \yii\db\Expression('NOW()'); $this->upsert('news', $entry, true); + } // delete not needed url routes foreach ($this->delete_url_routes as $route) { diff --git a/contrib/watchdog-action.py b/contrib/watchdog-action.py new file mode 100644 index 000000000..d5eaa9cf5 --- /dev/null +++ b/contrib/watchdog-action.py @@ -0,0 +1,33 @@ +#!/usr/local/bin/python3 +# +# pip install watchdog +import argparse +import os +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +# CLI arguments +parser = argparse.ArgumentParser() +parser.add_argument("--file_path", required=True, help="Full path to the file to monitor") +parser.add_argument("--action", required=True, help="Full path to the file we will execute") +args = parser.parse_args() + +FULL_PATH = args.file_path +FOLDER = os.path.dirname(FULL_PATH) +TARGET_FILE = os.path.basename(FULL_PATH) +ACTION = args.action + +class Handler(FileSystemEventHandler): + def __init__(self, observer): + self.observer = observer + + def on_created(self, event): + if not event.is_directory and event.src_path == FULL_PATH: + os.system(ACTION) + self.observer.stop() + +observer = Observer() +handler = Handler(observer) +observer.schedule(handler, FOLDER, recursive=False) +observer.start() +observer.join() diff --git a/contrib/watchdoger.py b/contrib/watchdoger.py new file mode 100644 index 000000000..38651de7a --- /dev/null +++ b/contrib/watchdoger.py @@ -0,0 +1,46 @@ +#!/usr/local/bin/python3 +# +# pip install watchdog requests +import argparse +import os +import requests +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +# CLI arguments +parser = argparse.ArgumentParser() +parser.add_argument("--file_path", required=True, help="Full path to the file to monitor") +parser.add_argument("--url", required=True, help="HTTP endpoint URL to POST to") +parser.add_argument("--token", required=True, help="Bearer token for authorization") +args = parser.parse_args() + +FULL_PATH = args.file_path +FOLDER = os.path.dirname(FULL_PATH) +TARGET_FILE = os.path.basename(FULL_PATH) +URL = args.url +BEARER_TOKEN = args.token + +class Handler(FileSystemEventHandler): + def __init__(self, observer): + self.observer = observer + + def on_created(self, event): + if not event.is_directory and event.src_path == FULL_PATH: + response = requests.post( + URL, + headers={ + "Authorization": f"Bearer {BEARER_TOKEN}", + "Content-Type": "application/json" + }, + json={ + "event": "apiNotifications" + } + ) + print(f"Posted {event.src_path}, status: {response.status_code}") + self.observer.stop() # exit after sending + +observer = Observer() +handler = Handler(observer) +observer.schedule(handler, FOLDER, recursive=False) +observer.start() +observer.join() diff --git a/docs/Websockets.md b/docs/Websockets.md new file mode 100644 index 000000000..3b46b4756 --- /dev/null +++ b/docs/Websockets.md @@ -0,0 +1,59 @@ +# Websockets service + +echoCTF.RED provides player updates to the live players through the use of [ws-server](https://github.com/echoCTF/ws-server). + +The services that want to communicate an update to the current live players submit their events through the HTTP service of ws-server. + +The system can send messages to a specific player or all connected players through the `/publish` and `/broadcast` endpoints respectively. + +Currently the following events are implemented: + +* `notification`: Sends a direct notification, Alert or Sweetalerts. +* `apiNotifications`: Tell the clients to perform an update of their in-page notifications. +* `target`: Update the target card if currently visible + +## Examples + +* Notify all users to perform an `apiNotifications()` js call. Effectively fetch the latest notifications through ajax. + +```shell +curl -X POST "http://localhost:8888/broadcast" \ + -H "Authorization: Bearer YOURTOKEN" \ + -H "Content-Type: application/json" \ + -d '{ "event": "apiNotifications" }' +``` + +* Send a SweetAlert (`"type": "swap:info"`) notification to player with id `1` + +```shell +curl -X POST "http://localhost:8888/publish" \ + -H "Authorization: Bearer server123token" \ + -H "Content-Type: application/json" \ + -d '{ + "player_id": "1", + "event": "notification", + "payload": + { + "title": "This is a notification", + "body": "This is the notification body", + "type": "swal:info" + } + }' +``` + +Note: Removing the `swal:` prefix from `type` sends a normal bootstrap alert notification. + +* Send an update for to player id `1` for updates on target id `2` + +```shell +curl -X POST "http://localhost:8888/publish" \ + -H "Authorization: Bearer server123token" \ + -H "Content-Type: application/json" \ + -d '{ + "player_id": "1", + "event": "target", + "payload": { "id": "2" } + }' +``` + +This will execute the js code `targetUpdates(2)`. diff --git a/frontend/commands/TesterController.php b/frontend/commands/TesterController.php deleted file mode 120000 index b00eb05cc..000000000 --- a/frontend/commands/TesterController.php +++ /dev/null @@ -1 +0,0 @@ -../../backend/commands/TesterController.php \ No newline at end of file diff --git a/frontend/commands/TesterController.php b/frontend/commands/TesterController.php new file mode 100644 index 000000000..4dc1dbdf3 --- /dev/null +++ b/frontend/commands/TesterController.php @@ -0,0 +1,88 @@ +stdout("*** TESTER COMMAND ***\n"); + + echo Table::widget([ + 'headers' => ['Action', 'Usage', 'Description'], + 'rows' => [ + ['Action' => 'tester/mail', 'Usage' => 'tester/mail email@example.com', 'Description' => 'Send a test mail with the current settings'], + ['Action' => 'tester/ws-notify', 'Usage' => 'tester/ws-notify player', 'Description' => 'Send a test websocket notification to the given player by id'], + ], + ]); + } + + /** + * Test the mailer configuration by sending a test email. + * + * Usage: + * backend tester/mail test@example.com + * + * @param string $to Recipient email + */ + public function actionMail($to) + { + $mailer = Yii::$app->mailer; + try { + + $this->stdout("*** SETTINGS *** \n"); + if (\Yii::$app->sys->mail_useFileTransport) { + $this->stdout("mail_useFileTransport: Yes\n"); + $this->stdout("mails folder: " . @\Yii::getAlias('@app/runtime/mail/') . "\n"); + } + if (\Yii::$app->sys->dsn) $this->stdout("dsn: " . \Yii::$app->sys->dsn . "\n"); + if (\Yii::$app->sys->mail_from) $this->stdout("mail_from: " . \Yii::$app->sys->mail_from . "\n"); + if (\Yii::$app->sys->mail_fromName) $this->stdout("mail_fromName: " . \Yii::$app->sys->mail_fromName . "\n"); + if (\Yii::$app->sys->mail_host) $this->stdout("mail_host: " . \Yii::$app->sys->mail_host . "\n"); + if (\Yii::$app->sys->mail_port) $this->stdout("mail_port: " . \Yii::$app->sys->mail_port . "\n"); + if (\Yii::$app->sys->mail_username) $this->stdout("mail_username: " . \Yii::$app->sys->mail_username . "\n"); + if (\Yii::$app->sys->mail_password) $this->stdout("mail_password: **USED BUT HIDDEN**\n"); + + $result = $mailer->compose() + ->setFrom([\Yii::$app->sys->mail_from => \Yii::$app->sys->mail_fromName]) + ->setTo($to) + ->setSubject('echoCTF Installation Mail Test') + ->setTextBody("This is a test email sent at " . date('Y-m-d H:i:s')) + ->send(); + if ($result) { + $this->stdout("✅ Test email successfully sent to {$to}\n"); + } else { + $this->stderr("❌ Failed to send test email to {$to}\n"); + } + } catch (\Symfony\Component\Mailer\Exception\TransportExceptionInterface $e) { + $this->stderr("❌ Transport error: " . $e->getMessage() . "\n"); + } catch (\Throwable $e) { + $this->stderr("❌ Error: " . $e->getMessage() . "\n"); + } + } + + public function actionWsNotify($player) + { + $player=\app\models\Player::findOne($player); + $type = "info"; + $title="title"; + $body="body"; + $cc = true; + $archive = true; + $apiOnly = false; + $player->notify($type, $title, $body, $cc, $archive, $apiOnly); + } + +} diff --git a/frontend/components/Img.php b/frontend/components/Img.php index f4371ad72..75126b2e1 100644 --- a/frontend/components/Img.php +++ b/frontend/components/Img.php @@ -55,12 +55,22 @@ public static function profile($profile) imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"root@%s:/# ./userinfo --profile %d"),\Yii::$app->sys->offense_domain,$profile->id),$textcolor); imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"username.....: %s"),$profile->owner->username),$greencolor); imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"joined.......: %s"),date("d.m.Y", strtotime($profile->owner->created))),$greencolor); - imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"points.......: %s"),number_format($profile->owner->playerScore->points)),$greencolor); - imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"rank.........: %s"),$profile->owner->playerScore->points == 0 ? "-":$profile->rank->ordinalPlace),$greencolor); - imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"level........: %d / %s"),$profile->experience->id, $profile->experience->name),$greencolor); - imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"flags........: %d"), $profile->totalTreasures),$greencolor); - imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"challenges...: %d / %d first"),$profile->challengesSolverCount, $profile->firstChallengeSolversCount),$greencolor); - imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"headshots....: %d / %d first"),$profile->headshotsCount, $profile->firstHeadshotsCount),$greencolor); + if (\Yii::$app->sys->team_only_leaderboards !== true) + { + imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"points.......: %s"),number_format($profile->owner->playerScore->points)),$greencolor); + imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"rank.........: %s"),$profile->owner->playerScore->points == 0 ? "-":$profile->rank->ordinalPlace),$greencolor); + imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"level........: %d / %s"),$profile->experience->id, $profile->experience->name),$greencolor); + imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"flags........: %d"), $profile->totalTreasures),$greencolor); + imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"challenges...: %d / %d first"),$profile->challengesSolverCount, $profile->firstChallengeSolversCount),$greencolor); + imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"headshots....: %d / %d first"),$profile->headshotsCount, $profile->firstHeadshotsCount),$greencolor); + } + else if($profile->owner->teamPlayer) + { + imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"team.........: {team}",['team'=>$profile->owner->team->name]),$greencolor); + imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"team rank....: {rank}",['rank'=>($profile->owner->team->rank !== null ? $profile->owner->team->rank->ordinalPlace : 'empty')]),$greencolor); + imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"team points..: {points,plural,=0{0 pts} =1{# pts} other{# pts}}",['points'=>($profile->owner->team->score !== null ? $profile->owner->team->score->points : 0)]),$greencolor); + imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"contributed..: {points,plural,=0{0 pts} =1{# pts} other{# pts}}",['points'=>($profile->owner->teamStreamPoints->points ?? 0)]),$greencolor); + } imagedestroy($avatar); imagedestroy($cover); imagedestroy($src); diff --git a/frontend/config/console.php b/frontend/config/console.php index 4b146d6c7..61ad81f0e 100644 --- a/frontend/config/console.php +++ b/frontend/config/console.php @@ -1,54 +1,60 @@ 'basic-console', -// 'language' => 'el-GR', - 'sourceLanguage' => 'en-US', - 'basePath' => dirname(__DIR__), - 'bootstrap' => ['log'], - 'controllerNamespace' => 'app\commands', - 'aliases' => [ - '@bower' => '@vendor/bower-asset', - '@npm' => '@vendor/npm-asset', - '@tests' => '@app/tests', +$config = [ + 'id' => 'basic-console', + // 'language' => 'el-GR', + 'sourceLanguage' => 'en-US', + 'basePath' => dirname(__DIR__), + 'bootstrap' => ['log'], + 'controllerNamespace' => 'app\commands', + 'aliases' => [ + '@bower' => '@vendor/bower-asset', + '@npm' => '@vendor/npm-asset', + '@tests' => '@app/tests', + ], + 'modules' => [ + 'team' => [ + 'class' => 'app\modules\team\Module', ], - 'components' => [ - 'i18n' => [ - 'translations' => [ - 'yii' => [ - 'class' => 'yii\i18n\PhpMessageSource', - ], - 'app*' => [ - 'class' => 'yii\i18n\PhpMessageSource', - 'basePath' => '@app/messages', - 'sourceLanguage' => 'en-US', - 'fileMap' => [ - 'app' => 'app.php', - 'app/error' => 'error.php', - ], - ], - ], + ], + + 'components' => [ + 'i18n' => [ + 'translations' => [ + 'yii' => [ + 'class' => 'yii\i18n\PhpMessageSource', ], - 'sys'=> [ - 'class' => 'app\components\Sysconfig', + 'app*' => [ + 'class' => 'yii\i18n\PhpMessageSource', + 'basePath' => '@app/messages', + 'sourceLanguage' => 'en-US', + 'fileMap' => [ + 'app' => 'app.php', + 'app/error' => 'error.php', + ], ], - 'cache' => $cache, - 'log' => [ - 'targets' => [ - [ - 'class' => 'yii\log\FileTarget', - 'levels' => ['error', 'warning'], - ], - ], + ], + ], + 'sys' => [ + 'class' => 'app\components\Sysconfig', + ], + 'cache' => $cache, + 'log' => [ + 'targets' => [ + [ + 'class' => 'yii\log\FileTarget', + 'levels' => ['error', 'warning'], ], - 'db' => $db, + ], ], - 'params' => $params, - /* + 'db' => $db, + ], + 'params' => $params, + /* 'controllerMap' => [ 'fixture' => [ // Fixture generation command line. 'class' => 'yii\faker\FixtureController', @@ -57,13 +63,12 @@ */ ]; -if(YII_ENV_DEV) -{ - // configuration adjustments for 'dev' environment - $config['bootstrap'][]='gii'; - $config['modules']['gii']=[ - 'class' => 'yii\gii\Module', - ]; +if (YII_ENV_DEV) { + // configuration adjustments for 'dev' environment + $config['bootstrap'][] = 'gii'; + $config['modules']['gii'] = [ + 'class' => 'yii\gii\Module', + ]; } return $config; diff --git a/frontend/models/Player.php b/frontend/models/Player.php index 0f4720f07..669cc5b3a 100644 --- a/frontend/models/Player.php +++ b/frontend/models/Player.php @@ -408,21 +408,25 @@ public function notify($type = "info", $title, $body, $cc = true, $archive = tru try { $publisher = new \app\services\ServerPublisher(Yii::$app->params['serverPublisher']); $publisher->publish($this->id, 'notification', ['type' => $type, 'title' => $title, 'body' => $body]); - } catch(\Throwable $e) { + } catch (\Throwable $e) { // on publishing error make sure we store the noticication as pending - $cc=true; - $archive=false; + $cc = true; + $archive = false; Yii::error($e->getMessage()); } if ($cc === true) { $n = new \app\models\Notification; $n->player_id = $this->id; - $n->archived = $archive; + $n->archived = intval($archive); $n->category = $type; $n->title = $title; $n->body = $body; - return $n->save(); + if (!$n->save()) { + Yii::error($n->getErrorSummary(true)); + return false; + } + return true; } return true; } diff --git a/frontend/modules/target/actions/SpawnRestAction.php b/frontend/modules/target/actions/SpawnRestAction.php index e7b0adb6d..916243e49 100644 --- a/frontend/modules/target/actions/SpawnRestAction.php +++ b/frontend/modules/target/actions/SpawnRestAction.php @@ -59,7 +59,7 @@ public function run($id,$team=false) $ti=new TargetInstance; $ti->player_id=Yii::$app->user->id; $ti->target_id=$id; - // pick the least used server currently + $ti->team_allowed=intval(\Yii::$app->sys->team_visible_instances); if(\Yii::$app->user->identity->subscription !== null && \Yii::$app->user->identity->subscription->active > 0 && \Yii::$app->user->identity->subscription->product !== null) { $metadata = json_decode(\Yii::$app->user->identity->subscription->product->metadata); @@ -67,6 +67,8 @@ public function run($id,$team=false) $ti->team_allowed=($team===false ? 0 : 1); } } + + // pick the least used server currently $ti->server_id=intval(Yii::$app->db->createCommand('select id from server t1 left join target_instance t2 on t1.id=t2.server_id group by t1.id order by count(t2.server_id) limit 1')->queryScalar()); if($ti->save()!==false) Yii::$app->session->setFlash('success', sprintf(\Yii::t('app','Spawning new instance for [%s]. You will receive a notification when the instance is up.'), $ti->target->name)); diff --git a/frontend/modules/target/models/Treasure.php b/frontend/modules/target/models/Treasure.php index f58bf6ccd..ccc44983b 100644 --- a/frontend/modules/target/models/Treasure.php +++ b/frontend/modules/target/models/Treasure.php @@ -146,7 +146,7 @@ public function getTarget() */ public function getLocationRedacted() { - return str_replace($this->code,"*REDACTED*",$this->location); + return str_replace($this->code,"*REDACTED*",$this->solution); } public static function find() diff --git a/frontend/modules/team/controllers/DefaultController.php b/frontend/modules/team/controllers/DefaultController.php index d64f9b9b4..4ca6c0a39 100644 --- a/frontend/modules/team/controllers/DefaultController.php +++ b/frontend/modules/team/controllers/DefaultController.php @@ -221,7 +221,6 @@ public function actionView($token) 'pageSize' => 10, ] ]); - return $this->render('view', [ 'team' => $model, 'teamInstanceProvider' => $teamInstanceProvider, @@ -264,8 +263,23 @@ public function actionMine() ] ]); + $teamNetworks = \app\modules\network\models\PrivateNetwork::find()->forTeam(\Yii::$app->user->identity->team->id); + $teamNetworksProvider = new ActiveDataProvider([ + 'query' => $teamNetworks, + 'pagination' => [ + 'pageSizeParam' => 'networks-perpage', + 'pageParam' => 'networks-page', + 'pageSize' => 5, + ], + 'sort' => ['defaultOrder' => ['name' => SORT_ASC]], + ]); + + $subQuery = TeamStream::find() + ->select('stream_id') + ->where(['team_id' => \Yii::$app->user->identity->team->id]); + $stream = \app\models\Stream::find()->select('stream.*,TS_AGO(ts) as ts_ago') - ->where(['stream.player_id' => $teamPlayers]) + ->where(['id' => $subQuery]) ->orderBy(['ts' => SORT_DESC, 'id' => SORT_DESC]); $streamProvider = new ActiveDataProvider([ 'query' => $stream, @@ -314,7 +328,9 @@ public function actionMine() 'teamTargetsProvider' => $targetProgressProvider, 'headshotsProvider' => $headshotsProvider, 'solverProvider' => $solverProvider, - 'team' => Yii::$app->user->identity->team + 'team' => Yii::$app->user->identity->team, + 'networksProvider' => $teamNetworksProvider, + ]); } /** diff --git a/frontend/modules/team/models/Team.php b/frontend/modules/team/models/Team.php index b84435fe7..258454e7b 100644 --- a/frontend/modules/team/models/Team.php +++ b/frontend/modules/team/models/Team.php @@ -25,7 +25,8 @@ * @property Player $owner * @property TeamPlayer[] $teamPlayers * @property Player[] $players - */ + * @property TeamInvite $inviteOrCreate +*/ class Team extends \yii\db\ActiveRecord { public $uploadedAvatar; @@ -142,7 +143,7 @@ public function getRank() */ public function getTeamPlayers() { - return $this->hasMany(TeamPlayer::class, ['team_id' => 'id'])->orderBy(['approved'=>SORT_DESC,'ts'=>SORT_ASC]); + return $this->hasMany(TeamPlayer::class, ['team_id' => 'id'])->orderBy(['approved' => SORT_DESC, 'ts' => SORT_ASC]); } /** @@ -161,6 +162,28 @@ public function getInvite() return $this->hasOne(TeamInvite::class, ['team_id' => 'id']); } + /** + * Returns the related TeamInvite model. + * + * If the invite does not exist yet, it will be created, saved, + * and populated into the `invite` relation. + * + * @return TeamInvite the existing or newly created invite model + * @throws \RuntimeException if the invite cannot be created + */ + public function getInviteOrCreate() + { + if ($this->invite === null) { + $invite = new TeamInvite(['team_id'=>$this->id,'token'=>Yii::$app->security->generateRandomString(8)]); + if (!$invite->save()) { + throw new \RuntimeException('Failed to create TeamInvite'); + } + $this->populateRelation('invite', $invite); + } + + return $this->invite; + } + public function getValidLogo() { if ($this->logo === null || trim($this->logo) === '') @@ -286,21 +309,19 @@ public function getAcademicWord() /** * Generate a new invite url */ - public function generate_invite(){ - if($this->invite) { - $this->invite->token=Yii::$app->security->generateRandomString(8); - if(!$this->invite->save()) - { - throw new UserException(Yii::t('app','Failed to save invite. [{error}]',['error'=>implode(" ",$this->invite->getErrors())])); + public function generate_invite() + { + if ($this->invite) { + $this->invite->token = Yii::$app->security->generateRandomString(8); + if (!$this->invite->save()) { + throw new UserException(Yii::t('app', 'Failed to save invite. [{error}]', ['error' => implode(" ", $this->invite->getErrors())])); } - } - else { - $ti=new TeamInvite; - $ti->team_id=$this->id; - $ti->token=Yii::$app->security->generateRandomString(8); - if(!$ti->save()) - { - throw new UserException(Yii::t('app','Failed to save invite. [{error}]',['error'=>implode(" ",$ti->getErrors())])); + } else { + $ti = new TeamInvite; + $ti->team_id = $this->id; + $ti->token = Yii::$app->security->generateRandomString(8); + if (!$ti->save()) { + throw new UserException(Yii::t('app', 'Failed to save invite. [{error}]', ['error' => implode(" ", $ti->getErrors())])); } } } diff --git a/frontend/themes/material/modules/target/views/default/_target_card.php b/frontend/themes/material/modules/target/views/default/_target_card.php index 284e2b9c8..53779fdcd 100644 --- a/frontend/themes/material/modules/target/views/default/_target_card.php +++ b/frontend/themes/material/modules/target/views/default/_target_card.php @@ -21,10 +21,10 @@ else $display_ip=Html::a($target_ip,$target_ip,["class"=>'copy-to-clipboard text-danger text-bold','swal-data'=>"Copied to clipboard",'data-toggle'=>'tooltip','title'=>\Yii::t('app',"The IP of your private instance. Click to copy IP to clipboard.")]); } -if($target_ip=='0.0.0.0') -{ - $this->registerJs("targetUpdates({$target->id});", \yii\web\View::POS_READY); -} +//if($target_ip=='0.0.0.0') +//{ +// $this->registerJs("targetUpdates({$target->id});", \yii\web\View::POS_READY); +//} $subtitleARR=[$target->category,ucfirst($target->getDifficultyText($target->average_rating)),boolval($target->rootable) ? "Rootable" : "Non rootable",$target->timer===false ? null:'Timed']; $subtitle=implode(", ",array_filter($subtitleARR)); Card::begin([ @@ -34,7 +34,7 @@ 'icon'=>sprintf('', $target->logo), 'color'=>'target', 'subtitle'=>$subtitle, - 'title'=>sprintf('%s / %s', $target->name, $display_ip), + 'title'=>sprintf('%s / %s', $target->name, $target->id, $display_ip), 'footer'=>sprintf('

%s
%s', $target->purpose, TargetCardActions::widget(['model'=>$target,'identity'=>$identity]) ), ]); echo "

", $target->total_treasures, ": Flag".($target->total_treasures > 1 ? 's' : '')." "; diff --git a/frontend/themes/material/modules/target/views/default/_target_metadata.php b/frontend/themes/material/modules/target/views/default/_target_metadata.php index f96d082ec..2ee835d76 100644 --- a/frontend/themes/material/modules/target/views/default/_target_metadata.php +++ b/frontend/themes/material/modules/target/views/default/_target_metadata.php @@ -1,11 +1,13 @@ user->isGuest && $metadata):?> + formatter->divID; ?> user->identity->isAdmin):?> - scenario)):?>: scenario,'gfm')?>
- instructions)):?>: instructions,'gfm')?>
- solution)):?>: solution,'gfm')?>
+ scenario)):?>: formatter->divID = 'markdown-scenario'; echo \Yii::$app->formatter->asMarkdown($metadata->scenario)?> + instructions)):?>: formatter->divID = 'markdown-instructions'; echo \Yii::$app->formatter->asMarkdown($metadata->instructions)?> + solution)):?>: formatter->divID = 'markdown-solution'; echo \Yii::$app->formatter->asMarkdown($metadata->solution)?> - pre_credits)):?>: pre_credits,'gfm')?>
- pre_exploitation)):?>: pre_exploitation,'gfm')?>
- player_id===Yii::$app->user->id && $target->progress==100) || Yii::$app->user->identity->isAdmin) && !empty($metadata->post_exploitation)):?>: post_exploitation,'gfm')?>
- player_id===Yii::$app->user->id && $target->progress==100) || Yii::$app->user->identity->isAdmin) && !empty($metadata->post_credits)):?>: post_credits,'gfm')?>
+ pre_credits)):?>: formatter->divID = 'markdown-pre-credits'; echo \Yii::$app->formatter->asMarkdown($metadata->pre_credits)?> + pre_exploitation)):?>: formatter->divID = 'markdown-pre-exploitation'; echo \Yii::$app->formatter->asMarkdown($metadata->pre_exploitation)?> + player_id===Yii::$app->user->id && $target->progress==100) || Yii::$app->user->identity->isAdmin) && !empty($metadata->post_exploitation)):?>: formatter->divID = 'markdown-post-exploitation'; echo \Yii::$app->formatter->asMarkdown($metadata->post_exploitation)?> + player_id===Yii::$app->user->id && $target->progress==100) || Yii::$app->user->identity->isAdmin) && !empty($metadata->post_credits)):?>: formatter->divID = 'markdown-post-credits'; echo \Yii::$app->formatter->asMarkdown($metadata->post_credits)?> + formatter->divID=$oldId; ?> diff --git a/frontend/themes/material/modules/target/views/default/_versus.php b/frontend/themes/material/modules/target/views/default/_versus.php index 83185c2f3..cc5e36332 100644 --- a/frontend/themes/material/modules/target/views/default/_versus.php +++ b/frontend/themes/material/modules/target/views/default/_versus.php @@ -29,6 +29,18 @@ $this->registerMetaTag(['name' => 'game:points', 'content' => '0']); $this->registerMetaTag(['name' => 'article:published_time', 'content' => $headshot->created_at]); } +$this->registerJsFile('@web/js/showdown.min.js',[ + 'depends' => [ + \yii\web\JqueryAsset::class + ] +]); +$this->registerJsFile('@web/assets/hljs/highlight.min.js',[ + 'depends' => [ + \yii\web\JqueryAsset::class + ] +]); +$this->registerCssFile('@web/assets/hljs/styles/a11y-dark.min.css',['depends' => ['app\assets\MaterialAsset']]); + ?>

@@ -167,4 +179,11 @@ render('_target_writeups', ['writeups' => $target->writeups, 'active' => false, 'writeups_activated' => (PTH::findOne(['player_id' => Yii::$app->user->id, 'target_id' => $target->id]) !== null || Headshot::findOne(['player_id' => Yii::$app->user->id, 'target_id' => $target->id]) !== null)]); ?>
-
\ No newline at end of file + +registerJs( + 'hljs.highlightAll();', + $this::POS_READY, + 'markdown-highlighter' +); +?> diff --git a/frontend/themes/material/modules/team/views/default/_team_card.php b/frontend/themes/material/modules/team/views/default/_team_card.php index 08939f0de..8b540334c 100644 --- a/frontend/themes/material/modules/team/views/default/_team_card.php +++ b/frontend/themes/material/modules/team/views/default/_team_card.php @@ -89,9 +89,9 @@
user->identity->isAdmin || (Yii::$app->user->identity->team && !$model->inviteonly || ($invite === false && $listing === true))) : ?> $model->token], ['class' => 'btn block text-dark text-bold orbitron' . (!$model->inviteonly ? ' btn-info' : ' btn-warning')]) ?> - getTeamPlayers()->count()) < Yii::$app->sys->members_per_team && !Yii::$app->user->identity->team && !$model->locked && $model->invite) : ?> - $model->invite->token], ['class' => 'btn block btn-primary text-dark text-bold orbitron', 'data-method' => 'POST', 'data' => ['confirm' => 'You are about to join this team. Your membership will have to be confirmed by the team captain.', 'method' => 'POST']]) ?> + getTeamPlayers()->count()) < Yii::$app->sys->members_per_team && !Yii::$app->user->identity->team && !$model->locked && $model->inviteOrCreate) : ?> + $model->inviteOrCreate->token], ['class' => 'btn block btn-primary text-dark text-bold orbitron', 'data-method' => 'POST', 'data' => ['confirm' => 'You are about to join this team. Your membership will have to be confirmed by the team captain.', 'method' => 'POST']]) ?>

- \ No newline at end of file + diff --git a/frontend/themes/material/modules/team/views/default/view.php b/frontend/themes/material/modules/team/views/default/view.php index 23e71ed62..4a34bd3be 100644 --- a/frontend/themes/material/modules/team/views/default/view.php +++ b/frontend/themes/material/modules/team/views/default/view.php @@ -19,10 +19,10 @@

[name) ?>]

getTeamPlayers()->count() < Yii::$app->sys->members_per_team): ?>

- owner_id === Yii::$app->user->id || ($team->invite && !$team->inviteonly)): ?> + owner_id === Yii::$app->user->id || ($team->inviteOrCreate && !$team->inviteonly)): ?> owner_id === Yii::$app->user->id) $class .= ' copy-to-clipboard'; ?> - $team->invite->token], 'https'), Url::to(['/team/default/invite', 'token' => $team->invite->token], 'https'), ['class' => $class, 'swal-data' => 'Copied to clipboard!']); ?> + $team->inviteOrCreate->token], 'https'), Url::to(['/team/default/invite', 'token' => $team->inviteOrCreate->token], 'https'), ['class' => $class, 'swal-data' => 'Copied to clipboard!']); ?> recruitment) ?> @@ -179,8 +179,8 @@

render('../_profile_tabs',['profile'=>$profile,'game'=>$game,'headshots'=>$headshots]);?> +sys->team_only_leaderboards!==true):?>
+ diff --git a/frontend/web/js/libechoctf.js b/frontend/web/js/libechoctf.js index d81836895..2741f9940 100644 --- a/frontend/web/js/libechoctf.js +++ b/frontend/web/js/libechoctf.js @@ -188,7 +188,7 @@ var targetTimeout; var intervalTimeout = 5000; function targetUpdates(id) { - var targetEl = document.getElementById("target_id"); + var targetEl = document.getElementById("target_id_"+id); if(targetEl==null) return; var request = new XMLHttpRequest(); request.open("GET", `/target/${id}/ip`); diff --git a/frontend/widgets/NotificationsWidget.php b/frontend/widgets/NotificationsWidget.php index d3d351a2b..d1b366d24 100644 --- a/frontend/widgets/NotificationsWidget.php +++ b/frontend/widgets/NotificationsWidget.php @@ -26,8 +26,12 @@ public function run() foreach($notifications as $n) { + if(intval($n->archived)===0) + $n->updateAttributes(['archived' => 1]); + $links[]=Html::a($n->title,'#',['class' => "dropdown-item"]); } + if($notifications==null) $links[]=Html::a('nothing here...','#',['class' => "dropdown-item"]); diff --git a/frontend/widgets/targetcardactions/TargetCardActions.php b/frontend/widgets/targetcardactions/TargetCardActions.php index a315e5b67..a34d0c071 100644 --- a/frontend/widgets/targetcardactions/TargetCardActions.php +++ b/frontend/widgets/targetcardactions/TargetCardActions.php @@ -92,7 +92,6 @@ public function prep_instance_actions() 'linkOptions' => $linkOptions ]; } elseif ($this->target_instance === NULL) { - $this->target_actions[] = [ 'label' => \Yii::t('app', '  Spawn a private instance'), 'url' => Url::to(['/target/default/spawn', 'id' => $this->model->id]), @@ -100,16 +99,20 @@ public function prep_instance_actions() 'linkOptions' => ArrayHelper::merge($this->linkOptions, ['data-confirm' => \Yii::t('app', 'You are about to spawn a private instance of this target. Once booted, this instance will only be accessible by you and its IP will become visible here.')]) ]; // display start team instance - if (\Yii::$app->user->identity->subscription !== null && \Yii::$app->user->identity->subscription->active > 0 && \Yii::$app->user->identity->subscription->product !== null) { - $metadata = json_decode(\Yii::$app->user->identity->subscription->product->metadata); - if (isset($metadata->team_subscription) && boolval($metadata->team_subscription) === true) { - $this->target_actions[] = [ - 'label' => \Yii::t('app', '  Spawn a team instance'), - 'url' => Url::to(['/target/default/spawn', 'id' => $this->model->id, 'team' => true]), - 'options' => ['style' => 'white-space: nowrap;'], - 'linkOptions' => ArrayHelper::merge($this->linkOptions, ['data-confirm' => \Yii::t('app', 'You are about to spawn an instance of this target for your entire team. Once booted, this instance will only be accessible by you and your team. The IP will become visible here.')]) - ]; - } + if ( + \Yii::$app->sys->team_visible_instances === true || (\Yii::$app->user->identity->subscription !== null + && \Yii::$app->user->identity->subscription->active > 0 + && \Yii::$app->user->identity->subscription->product !== null + && ($metadata = json_decode(\Yii::$app->user->identity->subscription->product->metadata) !== null + && isset($metadata->team_subscription) && boolval($metadata->team_subscription) === true)) + ) { + $key = \Yii::$app->sys->team_visible_instances === true ? 0 : null; + $this->target_actions[$key] = [ + 'label' => \Yii::t('app', '  Spawn a team instance'), + 'url' => Url::to(['/target/default/spawn', 'id' => $this->model->id, 'team' => true]), + 'options' => ['style' => 'white-space: nowrap;'], + 'linkOptions' => ArrayHelper::merge($this->linkOptions, ['data-confirm' => \Yii::t('app', 'You are about to spawn an instance of this target for your entire team. Once booted, this instance will only be accessible by you and your team. The IP will become visible here.')]) + ]; } } elseif ($this->target_instance->target_id !== $this->model->id) { $this->target_actions[] = [ @@ -168,7 +171,7 @@ public function prep_private_network_target_actions() if ($this->model->private_network_id === null) return; - if(\app\modules\network\models\PrivateNetwork::findOne(['id'=>$this->model->private_network_id,'player_id'=>Yii::$app->user->id])!==NULL) + if (\app\modules\network\models\PrivateNetwork::findOne(['id' => $this->model->private_network_id, 'player_id' => Yii::$app->user->id]) !== NULL) if ($this->model->ipoctet !== '0.0.0.0' && $this->model->ipoctet !== NULL && intval($this->model->state) === 0) { $this->target_actions[] = [ 'label' => \Yii::t('app', '  Reboot target'), diff --git a/schemas/echoCTF-routines.sql b/schemas/echoCTF-routines.sql index a289d74c9..7eb951c95 100644 --- a/schemas/echoCTF-routines.sql +++ b/schemas/echoCTF-routines.sql @@ -403,23 +403,16 @@ BEGIN UPDATE team_score SET points=0 WHERE team_id=tid; DELETE FROM team_stream WHERE team_id=tid; INSERT INTO team_stream (stream_id,player_id,team_id,model,model_id,points,ts) - SELECT id, player_id, tid, model, model_id, points, ts - FROM ( - SELECT s.*, - ROW_NUMBER() OVER ( - PARTITION BY model, model_id - ORDER BY ts ASC, id ASC - ) AS rn - FROM stream s - WHERE model != 'user' - AND player_id IN ( - SELECT player_id - FROM team_player - WHERE team_id = tid AND approved=1 - ) - ) t - WHERE rn = 1 - ORDER BY id, ts; + SELECT id, player_id, tid, model, model_id, points, ts + FROM ( + SELECT s.*, + ROW_NUMBER() OVER (PARTITION BY model, model_id ORDER BY ts ASC, id ASC) AS rn + FROM stream s + WHERE model != 'user' + AND player_id IN (SELECT player_id FROM team_player WHERE team_id = tid AND approved=1) + ) t + WHERE rn = 1 + ORDER BY id, ts; IF `_rollback` THEN ROLLBACK; ELSE diff --git a/schemas/echoCTF-triggers.sql b/schemas/echoCTF-triggers.sql index b558677d4..655d886c6 100644 --- a/schemas/echoCTF-triggers.sql +++ b/schemas/echoCTF-triggers.sql @@ -127,38 +127,49 @@ CREATE TRIGGER `tbi_headshot` BEFORE INSERT ON `headshot` FOR EACH ROW END IF; END ;; -DROP TRIGGER IF EXISTS tai_headshot ;; +DROP TRIGGER IF EXISTS `tai_headshot` ;; CREATE TRIGGER `tai_headshot` AFTER INSERT ON `headshot` FOR EACH ROW - thisBegin:BEGIN - DECLARE private_instance int; - IF (@TRIGGER_CHECKS = FALSE) THEN - LEAVE thisBegin; - END IF; - SET private_instance=(SELECT COUNT(*) FROM target_instance WHERE player_id=NEW.player_id AND target_id=NEW.target_id); - IF (SELECT headshot_spin FROM target WHERE id=NEW.target_id)>0 AND private_instance<1 THEN - INSERT IGNORE INTO spin_queue (target_id, player_id,created_at) VALUES (NEW.target_id,NEW.player_id,NOW()); - ELSEIF private_instance>0 THEN - UPDATE target_instance SET reboot=2 WHERE player_id=NEW.player_id AND target_id=NEW.target_id; - END IF; - IF (SELECT count(*) FROM target_ondemand WHERE target_id=NEW.target_id AND state=1)>0 THEN - UPDATE target_ondemand SET heartbeat=(NOW() - INTERVAL 59 MINUTE - INTERVAL 30 SECOND) WHERE target_id=NEW.target_id; - END IF; - UPDATE target_state SET total_headshots=total_headshots+1,timer_avg=(SELECT ifnull(round(avg(timer)),0) FROM headshot WHERE target_id=NEW.target_id) WHERE id=NEW.target_id; - END ;; + thisBegin:BEGIN + DECLARE private_instance int; + DECLARE local_points int; + DECLARE lheadshot_points int; + DECLARE lfirst_headshot_points int; + IF (@TRIGGER_CHECKS = FALSE) THEN + LEAVE thisBegin; + END IF; + SET local_points=0; + SELECT headshot_points,first_headshot_points INTO lheadshot_points,lfirst_headshot_points FROM target WHERE id=NEW.target_id; + SET private_instance=(SELECT COUNT(*) FROM target_instance WHERE player_id=NEW.player_id AND target_id=NEW.target_id); + IF (SELECT headshot_spin FROM target WHERE id=NEW.target_id)>0 AND private_instance<1 THEN + INSERT IGNORE INTO spin_queue (target_id, player_id,created_at) VALUES (NEW.target_id,NEW.player_id,NOW()); + ELSEIF private_instance>0 THEN + UPDATE target_instance SET reboot=2 WHERE player_id=NEW.player_id AND target_id=NEW.target_id; + END IF; + IF (SELECT count(*) FROM target_ondemand WHERE target_id=NEW.target_id AND state=1)>0 THEN + UPDATE target_ondemand SET heartbeat=(NOW() - INTERVAL 59 MINUTE - INTERVAL 30 SECOND) WHERE target_id=NEW.target_id; + END IF; + IF NEW.first = 1 AND lfirst_headshot_points>0 THEN + SET local_points=lfirst_headshot_points; + ELSEIF lheadshot_points>0 THEN + SET local_points=lheadshot_points; + END IF; + INSERT INTO stream (player_id,model,model_id,points,title,message,pubtitle,pubmessage,ts) VALUES (NEW.player_id,'headshot',NEW.target_id,local_points,'','','','',now()); + UPDATE target_state SET total_headshots=total_headshots+1,timer_avg=(SELECT ifnull(round(avg(timer)),0) FROM headshot WHERE target_id=NEW.target_id) WHERE id=NEW.target_id; + END ;; DROP TRIGGER IF EXISTS tau_headshot ;; CREATE TRIGGER `tau_headshot` AFTER UPDATE ON `headshot` FOR EACH ROW - thisBegin:BEGIN - IF (@TRIGGER_CHECKS = FALSE) THEN - LEAVE thisBegin; - END IF; +thisBegin:BEGIN + IF (@TRIGGER_CHECKS = FALSE) THEN + LEAVE thisBegin; + END IF; IF (OLD.rating IS NULL AND NEW.rating IS NOT NULL) OR (OLD.rating IS NOT NULL and NEW.rating!=OLD.rating) THEN UPDATE target_state SET player_rating=(SELECT round(avg(rating)) FROM headshot WHERE target_id=NEW.target_id AND rating>-1) WHERE id=NEW.target_id; END IF; IF (OLD.timer IS NULL AND NEW.timer IS NOT NULL) OR (OLD.timer IS NOT NULL AND NEW.timer!=OLD.timer) THEN - UPDATE target_state SET timer_avg=(SELECT ifnull(round(avg(timer)),0) FROM headshot WHERE target_id=NEW.target_id and timer>60) WHERE id=NEW.target_id; + UPDATE target_state SET timer_avg=(SELECT ifnull(round(avg(timer)),0) FROM headshot WHERE target_id=NEW.target_id and timer>60) WHERE id=NEW.target_id; END IF; - END ;; + END ;; DROP TRIGGER IF EXISTS tad_headshot ;; CREATE TRIGGER `tad_headshot` AFTER DELETE ON `headshot` FOR EACH ROW @@ -214,7 +225,7 @@ CREATE TRIGGER tai_player AFTER INSERT ON player FOR EACH ROW DROP TRIGGER IF EXISTS tau_player ;; CREATE TRIGGER `tau_player` AFTER UPDATE ON `player` FOR EACH ROW - thisBegin:BEGIN +thisBegin:BEGIN DECLARE ltitle VARCHAR(30) DEFAULT 'Joined the platform'; IF (@TRIGGER_CHECKS = FALSE) THEN LEAVE thisBegin; @@ -249,23 +260,23 @@ CREATE TRIGGER `tau_player` AFTER UPDATE ON `player` FOR EACH ROW ELSEIF NEW.status=10 AND (OLD.status=9 OR OLD.status=8) THEN DELETE FROM player_token WHERE player_id=NEW.id AND `type`='email_verification'; END IF; - END ;; +END ;; DROP TRIGGER IF EXISTS tbd_player ;; CREATE TRIGGER `tbd_player` BEFORE DELETE ON `player` FOR EACH ROW - thisBegin:BEGIN - DECLARE tid INT default 0; - IF (@TRIGGER_CHECKS = FALSE) THEN - LEAVE thisBegin; - END IF; - SELECT id INTO tid FROM team where owner_id=OLD.id; - IF tid > 0 THEN - DELETE FROM team_score WHERE team_id=tid; - END IF; - DELETE FROM player_ssl WHERE player_id=OLD.id; - DELETE FROM player_rank WHERE player_id=OLD.id; +thisBegin:BEGIN + DECLARE tid INT default 0; + IF (@TRIGGER_CHECKS = FALSE) THEN + LEAVE thisBegin; + END IF; + SELECT id INTO tid FROM team where owner_id=OLD.id; + IF tid > 0 THEN + DELETE FROM team_score WHERE team_id=tid; + END IF; + DELETE FROM player_ssl WHERE player_id=OLD.id; + DELETE FROM player_rank WHERE player_id=OLD.id; DELETE FROM headshot WHERE player_id=OLD.id; - END ;; +END ;; DROP TRIGGER IF EXISTS tad_player ;; CREATE TRIGGER `tad_player` AFTER DELETE ON `player` FOR EACH ROW @@ -365,7 +376,7 @@ CREATE TRIGGER `tbi_player_finding` BEFORE INSERT ON `player_finding` FOR EACH R DROP TRIGGER IF EXISTS tai_player_finding ;; CREATE TRIGGER `tai_player_finding` AFTER INSERT ON `player_finding` FOR EACH ROW - thisBegin:BEGIN +thisBegin:BEGIN DECLARE local_target_id INT; DECLARE headshoted INT default null; DECLARE min_finding,min_treasure,max_finding,max_treasure, max_val, min_val DATETIME; @@ -391,7 +402,7 @@ CREATE TRIGGER `tai_player_finding` AFTER INSERT ON `player_finding` FOR EACH RO INSERT INTO headshot (player_id,target_id,created_at,timer) VALUES (NEW.player_id,local_target_id,now(),UNIX_TIMESTAMP(max_val)-UNIX_TIMESTAMP(min_val)); END IF; INSERT INTO target_player_state (id,player_id,player_findings,player_points,created_at,updated_at) VALUES (local_target_id,NEW.player_id,1,NEW.points,now(),now()) ON DUPLICATE KEY UPDATE player_findings=player_findings+values(player_findings),player_points=player_points+values(player_points),updated_at=now(); - END ;; +END ;; DROP TRIGGER IF EXISTS tad_player_finding ;; CREATE TRIGGER `tad_player_finding` AFTER DELETE ON `player_finding` FOR EACH ROW @@ -408,25 +419,25 @@ END ;; DROP TRIGGER IF EXISTS tau_player_last ;; CREATE TRIGGER `tau_player_last` AFTER UPDATE ON `player_last` FOR EACH ROW - thisBegin:BEGIN - IF (@TRIGGER_CHECKS = FALSE) THEN - LEAVE thisBegin; - END IF; +thisBegin:BEGIN + IF (@TRIGGER_CHECKS = FALSE) THEN + LEAVE thisBegin; + END IF; - IF (OLD.vpn_local_address IS NULL AND NEW.vpn_local_address IS NOT NULL) THEN - DO memc_set(CONCAT('ovpn:',NEW.id),INET_NTOA(NEW.vpn_local_address)); - DO memc_set(CONCAT('ovpn:',INET_NTOA(NEW.vpn_local_address)),NEW.id); - DO memc_set(CONCAT('ovpn_remote:',NEW.id),INET_NTOA(NEW.vpn_remote_address)); - ELSEIF (OLD.vpn_local_address IS NOT NULL AND NEW.vpn_local_address IS NULL) THEN - DO memc_delete(CONCAT('ovpn:',NEW.id)); - DO memc_delete(CONCAT('ovpn_remote:',NEW.id)); - DO memc_delete(CONCAT('ovpn:',INET_NTOA(OLD.vpn_local_address))); - END IF; + IF (OLD.vpn_local_address IS NULL AND NEW.vpn_local_address IS NOT NULL) THEN + DO memc_set(CONCAT('ovpn:',NEW.id),INET_NTOA(NEW.vpn_local_address)); + DO memc_set(CONCAT('ovpn:',INET_NTOA(NEW.vpn_local_address)),NEW.id); + DO memc_set(CONCAT('ovpn_remote:',NEW.id),INET_NTOA(NEW.vpn_remote_address)); + ELSEIF (OLD.vpn_local_address IS NOT NULL AND NEW.vpn_local_address IS NULL) THEN + DO memc_delete(CONCAT('ovpn:',NEW.id)); + DO memc_delete(CONCAT('ovpn_remote:',NEW.id)); + DO memc_delete(CONCAT('ovpn:',INET_NTOA(OLD.vpn_local_address))); + END IF; - IF (OLD.vpn_local_address IS NULL AND NEW.vpn_local_address IS NOT NULL) OR (OLD.vpn_local_address IS NOT NULL AND NEW.vpn_local_address IS NOT NULL AND NEW.vpn_local_address!=OLD.vpn_local_address) THEN - INSERT INTO `player_vpn_history` (`player_id`,`vpn_local_address`,`vpn_remote_address`) VALUES (NEW.id,NEW.vpn_local_address,NEW.vpn_remote_address); - END IF; - END ;; + IF (OLD.vpn_local_address IS NULL AND NEW.vpn_local_address IS NOT NULL) OR (OLD.vpn_local_address IS NOT NULL AND NEW.vpn_local_address IS NOT NULL AND NEW.vpn_local_address!=OLD.vpn_local_address) THEN + INSERT INTO `player_vpn_history` (`player_id`,`vpn_local_address`,`vpn_remote_address`) VALUES (NEW.id,NEW.vpn_local_address,NEW.vpn_remote_address); + END IF; +END ;; DROP TRIGGER IF EXISTS tai_player_question ;; CREATE TRIGGER `tai_player_question` AFTER INSERT ON `player_question` FOR EACH ROW @@ -507,15 +518,6 @@ CREATE TRIGGER `tau_player_token` AFTER UPDATE ON `player_token` FOR EACH ROW END IF; END ;; -DROP TRIGGER IF EXISTS tad_player_token ;; -CREATE TRIGGER `tad_player_token` AFTER DELETE ON `player_token` FOR EACH ROW - thisBegin:BEGIN - IF (@TRIGGER_CHECKS = FALSE) THEN - LEAVE thisBegin; - END IF; - INSERT INTO player_token_history (player_id,`type`,token,description,expires_at,created_at,ts) VALUES (OLD.player_id,OLD.type,OLD.token,OLD.description,OLD.expires_at,OLD.created_at,NOW()); - END ;; - DROP TRIGGER IF EXISTS tbi_player_treasure ;; CREATE TRIGGER `tbi_player_treasure` BEFORE INSERT ON `player_treasure` FOR EACH ROW thisBegin:BEGIN @@ -533,35 +535,35 @@ CREATE TRIGGER `tbi_player_treasure` BEFORE INSERT ON `player_treasure` FOR EACH DROP TRIGGER IF EXISTS tai_player_treasure ;; CREATE TRIGGER `tai_player_treasure` AFTER INSERT ON `player_treasure` FOR EACH ROW - thisBegin:BEGIN - DECLARE local_target_id INT; - DECLARE headshoted INT default null; +thisBegin:BEGIN + DECLARE local_target_id INT; + DECLARE headshoted INT default null; DECLARE tfindings,pfindings INT; - DECLARE min_finding,min_treasure,max_finding,max_treasure, max_val, min_val DATETIME; + DECLARE min_finding,min_treasure,max_finding,max_treasure, max_val, min_val DATETIME; - IF (@TRIGGER_CHECKS = FALSE) THEN - LEAVE thisBegin; - END IF; + IF (@TRIGGER_CHECKS = FALSE) THEN + LEAVE thisBegin; + END IF; - CALL add_treasure_stream(NEW.player_id,'treasure',NEW.treasure_id,NEW.points); - CALL add_player_treasure_hint(NEW.player_id,NEW.treasure_id); + CALL add_treasure_stream(NEW.player_id,'treasure',NEW.treasure_id,NEW.points); + CALL add_player_treasure_hint(NEW.player_id,NEW.treasure_id); - SET local_target_id=(SELECT target_id FROM treasure WHERE id=NEW.treasure_id); - SET headshoted=(select true as headshoted FROM target as t left join treasure as t2 on t2.target_id=t.id left join finding as t3 on t3.target_id=t.id LEFT JOIN player_treasure as t4 on t4.treasure_id=t2.id and t4.player_id=NEW.player_id left join player_finding as t5 on t5.finding_id=t3.id and t5.player_id=NEW.player_id WHERE t.id=local_target_id GROUP BY t.id HAVING count(distinct t2.id)=count(distinct t4.treasure_id) AND count(distinct t3.id)=count(distinct t5.finding_id)); + SET local_target_id=(SELECT target_id FROM treasure WHERE id=NEW.treasure_id); + SET headshoted=(select true as headshoted FROM target as t left join treasure as t2 on t2.target_id=t.id left join finding as t3 on t3.target_id=t.id LEFT JOIN player_treasure as t4 on t4.treasure_id=t2.id and t4.player_id=NEW.player_id left join player_finding as t5 on t5.finding_id=t3.id and t5.player_id=NEW.player_id WHERE t.id=local_target_id GROUP BY t.id HAVING count(distinct t2.id)=count(distinct t4.treasure_id) AND count(distinct t3.id)=count(distinct t5.finding_id)); SET tfindings=(SELECT count(*) FROM finding WHERE target_id=local_target_id); SET pfindings=(SELECT count(*) FROM player_finding WHERE player_id=NEW.player_id AND finding_id IN (SELECT id FROM finding WHERE target_id=local_target_id)); - IF headshoted IS NOT NULL THEN - SELECT min(ts),max(ts) INTO min_finding,max_finding FROM player_finding WHERE player_id=NEW.player_id AND finding_id IN (SELECT id FROM finding WHERE target_id=local_target_id); - SELECT min(ts),max(ts) INTO min_treasure,max_treasure FROM player_treasure WHERE player_id=NEW.player_id AND treasure_id IN (SELECT id FROM treasure WHERE target_id=local_target_id); - SELECT GREATEST(max_finding, max_treasure), LEAST(min_finding, min_treasure) INTO max_val,min_val; - INSERT INTO headshot (player_id,target_id,created_at,timer) VALUES (NEW.player_id,local_target_id,now(),UNIX_TIMESTAMP(max_val)-UNIX_TIMESTAMP(min_val)); - END IF; - INSERT INTO target_player_state (id,player_id,player_treasures,player_points,created_at,updated_at) VALUES (local_target_id,NEW.player_id,1,NEW.points,now(),now()) ON DUPLICATE KEY UPDATE player_treasures=player_treasures+values(player_treasures),player_points=player_points+values(player_points),updated_at=now(); + IF headshoted IS NOT NULL THEN + SELECT min(ts),max(ts) INTO min_finding,max_finding FROM player_finding WHERE player_id=NEW.player_id AND finding_id IN (SELECT id FROM finding WHERE target_id=local_target_id); + SELECT min(ts),max(ts) INTO min_treasure,max_treasure FROM player_treasure WHERE player_id=NEW.player_id AND treasure_id IN (SELECT id FROM treasure WHERE target_id=local_target_id); + SELECT GREATEST(max_finding, max_treasure), LEAST(min_finding, min_treasure) INTO max_val,min_val; + INSERT INTO headshot (player_id,target_id,created_at,timer) VALUES (NEW.player_id,local_target_id,now(),UNIX_TIMESTAMP(max_val)-UNIX_TIMESTAMP(min_val)); + END IF; + INSERT INTO target_player_state (id,player_id,player_treasures,player_points,created_at,updated_at) VALUES (local_target_id,NEW.player_id,1,NEW.points,now(),now()) ON DUPLICATE KEY UPDATE player_treasures=player_treasures+values(player_treasures),player_points=player_points+values(player_points),updated_at=now(); IF tfindings > 0 AND pfindings = 0 THEN INSERT INTO abuser (player_id,title,reason,model,model_id,created_at,updated_at) VALUES (NEW.player_id,'Claim flag before finding','tai_player_treasure','treasure',NEW.treasure_id,NOW(),NOW()); END IF; - END ;; +END ;; DROP TRIGGER IF EXISTS tbi_profile ;; CREATE TRIGGER `tbi_profile` BEFORE INSERT ON `profile` FOR EACH ROW @@ -611,29 +613,29 @@ END ;; DROP TRIGGER IF EXISTS tai_stream ;; CREATE TRIGGER `tai_stream` AFTER INSERT ON `stream` FOR EACH ROW - thisBegin:BEGIN - DECLARE lteam_id INT; - IF (@TRIGGER_CHECKS = FALSE) THEN - LEAVE thisBegin; - END IF; - IF NEW.points>0 THEN - INSERT INTO player_score (player_id,points) VALUES (NEW.player_id,NEW.points) ON DUPLICATE KEY UPDATE points=points+values(points); - END IF; - SELECT team_id INTO lteam_id FROM team_player WHERE player_id=NEW.player_id AND approved=1; - IF lteam_id IS NOT NULL THEN +thisBegin:BEGIN + DECLARE lteam_id INT; + IF (@TRIGGER_CHECKS = FALSE) THEN + LEAVE thisBegin; + END IF; + IF NEW.points>0 THEN + INSERT INTO player_score (player_id,points) VALUES (NEW.player_id,NEW.points) ON DUPLICATE KEY UPDATE points=points+values(points); + END IF; + SELECT team_id INTO lteam_id FROM team_player WHERE player_id=NEW.player_id AND approved=1; + IF lteam_id IS NOT NULL THEN INSERT IGNORE INTO team_stream (stream_id,player_id,team_id,model,model_id,points,ts) VALUES (NEW.id,NEW.player_id,lteam_id,NEW.model,NEW.model_id,NEW.points,NEW.ts); - END IF; - END ;; + END IF; +END ;; DROP TRIGGER IF EXISTS tad_stream ;; CREATE TRIGGER `tad_stream` AFTER DELETE ON `stream` FOR EACH ROW - thisBegin:BEGIN - IF (@TRIGGER_CHECKS = FALSE) THEN - LEAVE thisBegin; - END IF; - INSERT INTO player_score (player_id,points) VALUES (OLD.player_id,-OLD.points) ON DUPLICATE KEY UPDATE points=if(points+values(points)<0,0,points+values(points)); +thisBegin:BEGIN + IF (@TRIGGER_CHECKS = FALSE) THEN + LEAVE thisBegin; + END IF; + INSERT INTO player_score (player_id,points) VALUES (OLD.player_id,-OLD.points) ON DUPLICATE KEY UPDATE points=if(points+values(points)<0,0,points+values(points)); DELETE FROM team_stream where stream_id=OLD.id; - END ;; +END ;; DROP TRIGGER IF EXISTS tai_sysconfig ;; CREATE TRIGGER `tai_sysconfig` AFTER INSERT ON `sysconfig` FOR EACH ROW @@ -976,12 +978,16 @@ CREATE TRIGGER `tad_writeup` AFTER DELETE ON `writeup` FOR EACH ROW DROP TRIGGER IF EXISTS tai_player_target_help ;; CREATE TRIGGER tai_player_target_help AFTER INSERT ON player_target_help FOR EACH ROW - thisBegin:BEGIN +thisBegin:BEGIN + DECLARE stream_player_target_help INT; IF (@TRIGGER_CHECKS = FALSE) THEN LEAVE thisBegin; END IF; - INSERT INTO stream (player_id,model,model_id,points,title,message,pubtitle,pubmessage,ts) VALUES (NEW.player_id,'player_target_help',NEW.target_id,0,'','','','',now()); - END ;; + SET stream_player_target_help=memc_get('sysconfig:stream_player_target_help'); + IF stream_player_target_help IS NOT NULL and stream_player_target_help=1 THEN + INSERT INTO stream (player_id,model,model_id,points,title,message,pubtitle,pubmessage,ts) VALUES (NEW.player_id,'player_target_help',NEW.target_id,0,'Activated writeups for {target}','','','',now()); + END IF; +END ;; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;