├── include ├── thirdparty │ ├── composer │ │ └── .gitignore │ └── nbbc │ │ └── LICENSE ├── config │ ├── .gitignore │ └── db.default.inc.php ├── config_loader.inc.php ├── xsrf.inc.php ├── Config.php ├── raceconditions.inc.php ├── captcha.inc.php ├── language │ └── language.inc.php ├── mellivora.inc.php ├── layout │ ├── dynamic.inc.php │ ├── login_dialog.inc.php │ └── messages.inc.php ├── json.inc.php ├── two_factor_auth.inc.php ├── constants.inc.php └── cache.inc.php ├── tests ├── codeception │ ├── _data │ │ └── sql │ │ │ ├── .gitignore │ │ │ ├── setup │ │ │ └── create_database.sql │ │ │ └── acceptance │ │ │ ├── categories.sql │ │ │ ├── users.sql │ │ │ └── challenges.sql │ ├── _output │ │ └── .gitignore │ ├── _bootstrap.php │ ├── unit │ │ ├── _bootstrap.php │ │ └── GeneralTest.php │ ├── functional │ │ └── _bootstrap.php │ ├── acceptance │ │ ├── admin │ │ │ ├── BasicAdminCest.php │ │ │ └── ManageNewsCest.php │ │ ├── RegisterCest.php │ │ ├── _bootstrap.php │ │ └── SubmitFlagCest.php │ ├── unit.suite.yml │ ├── _support │ │ ├── Helper │ │ │ ├── Unit.php │ │ │ ├── Acceptance.php │ │ │ └── Functional.php │ │ ├── UnitTester.php │ │ └── FunctionalTester.php │ ├── functional.suite.yml │ └── acceptance.suite.yml ├── build_sql_dump ├── README.md └── run_tests ├── htdocs ├── favicon.ico ├── img │ ├── json.png │ ├── stop.png │ ├── accept.png │ ├── favicon.png │ ├── flags │ │ ├── ad.png │ │ ├── ae.png │ │ ├── af.png │ │ ├── ag.png │ │ ├── ai.png │ │ ├── al.png │ │ ├── am.png │ │ ├── ao.png │ │ ├── aq.png │ │ ├── ar.png │ │ ├── as.png │ │ ├── at.png │ │ ├── au.png │ │ ├── aw.png │ │ ├── ax.png │ │ ├── az.png │ │ ├── ba.png │ │ ├── bb.png │ │ ├── bd.png │ │ ├── be.png │ │ ├── bf.png │ │ ├── bg.png │ │ ├── bh.png │ │ ├── bi.png │ │ ├── bj.png │ │ ├── bl.png │ │ ├── bm.png │ │ ├── bn.png │ │ ├── bo.png │ │ ├── bq.png │ │ ├── br.png │ │ ├── bs.png │ │ ├── bt.png │ │ ├── bv.png │ │ ├── bw.png │ │ ├── by.png │ │ ├── bz.png │ │ ├── ca.png │ │ ├── cc.png │ │ ├── cd.png │ │ ├── cf.png │ │ ├── cg.png │ │ ├── ch.png │ │ ├── ci.png │ │ ├── ck.png │ │ ├── cl.png │ │ ├── cm.png │ │ ├── cn.png │ │ ├── co.png │ │ ├── cr.png │ │ ├── cu.png │ │ ├── cv.png │ │ ├── cw.png │ │ ├── cx.png │ │ ├── cy.png │ │ ├── cz.png │ │ ├── de.png │ │ ├── dj.png │ │ ├── dk.png │ │ ├── dm.png │ │ ├── do.png │ │ ├── dz.png │ │ ├── ec.png │ │ ├── ee.png │ │ ├── eg.png │ │ ├── eh.png │ │ ├── er.png │ │ ├── es.png │ │ ├── et.png │ │ ├── fi.png │ │ ├── fj.png │ │ ├── fk.png │ │ ├── fm.png │ │ ├── fo.png │ │ ├── fr.png │ │ ├── ga.png │ │ ├── gb.png │ │ ├── gd.png │ │ ├── ge.png │ │ ├── gf.png │ │ ├── gg.png │ │ ├── gh.png │ │ ├── gi.png │ │ ├── gl.png │ │ ├── gm.png │ │ ├── gn.png │ │ ├── gp.png │ │ ├── gq.png │ │ ├── gr.png │ │ ├── gs.png │ │ ├── gt.png │ │ ├── gu.png │ │ ├── gw.png │ │ ├── gy.png │ │ ├── hk.png │ │ ├── hm.png │ │ ├── hn.png │ │ ├── hr.png │ │ ├── ht.png │ │ ├── hu.png │ │ ├── id.png │ │ ├── ie.png │ │ ├── il.png │ │ ├── im.png │ │ ├── in.png │ │ ├── io.png │ │ ├── iq.png │ │ ├── ir.png │ │ ├── is.png │ │ ├── it.png │ │ ├── je.png │ │ ├── jm.png │ │ ├── jo.png │ │ ├── jp.png │ │ ├── ke.png │ │ ├── kg.png │ │ ├── kh.png │ │ ├── ki.png │ │ ├── km.png │ │ ├── kn.png │ │ ├── kp.png │ │ ├── kr.png │ │ ├── kw.png │ │ ├── ky.png │ │ ├── kz.png │ │ ├── la.png │ │ ├── lb.png │ │ ├── lc.png │ │ ├── li.png │ │ ├── lk.png │ │ ├── lr.png │ │ ├── ls.png │ │ ├── lt.png │ │ ├── lu.png │ │ ├── lv.png │ │ ├── ly.png │ │ ├── ma.png │ │ ├── mc.png │ │ ├── md.png │ │ ├── me.png │ │ ├── mf.png │ │ ├── mg.png │ │ ├── mh.png │ │ ├── mk.png │ │ ├── ml.png │ │ ├── mm.png │ │ ├── mn.png │ │ ├── mo.png │ │ ├── mp.png │ │ ├── mq.png │ │ ├── mr.png │ │ ├── ms.png │ │ ├── mt.png │ │ ├── mu.png │ │ ├── mv.png │ │ ├── mw.png │ │ ├── mx.png │ │ ├── my.png │ │ ├── mz.png │ │ ├── na.png │ │ ├── nc.png │ │ ├── ne.png │ │ ├── nf.png │ │ ├── ng.png │ │ ├── ni.png │ │ ├── nl.png │ │ ├── no.png │ │ ├── np.png │ │ ├── nr.png │ │ ├── nu.png │ │ ├── nz.png │ │ ├── om.png │ │ ├── pa.png │ │ ├── pe.png │ │ ├── pf.png │ │ ├── pg.png │ │ ├── ph.png │ │ ├── pk.png │ │ ├── pl.png │ │ ├── pm.png │ │ ├── pn.png │ │ ├── pr.png │ │ ├── ps.png │ │ ├── pt.png │ │ ├── pw.png │ │ ├── py.png │ │ ├── qa.png │ │ ├── re.png │ │ ├── ro.png │ │ ├── rs.png │ │ ├── ru.png │ │ ├── rw.png │ │ ├── sa.png │ │ ├── sb.png │ │ ├── sc.png │ │ ├── sd.png │ │ ├── se.png │ │ ├── sg.png │ │ ├── sh.png │ │ ├── si.png │ │ ├── sj.png │ │ ├── sk.png │ │ ├── sl.png │ │ ├── sm.png │ │ ├── sn.png │ │ ├── so.png │ │ ├── sr.png │ │ ├── ss.png │ │ ├── st.png │ │ ├── sv.png │ │ ├── sx.png │ │ ├── sy.png │ │ ├── sz.png │ │ ├── tc.png │ │ ├── td.png │ │ ├── tf.png │ │ ├── tg.png │ │ ├── th.png │ │ ├── tj.png │ │ ├── tk.png │ │ ├── tl.png │ │ ├── tm.png │ │ ├── tn.png │ │ ├── to.png │ │ ├── tr.png │ │ ├── tt.png │ │ ├── tv.png │ │ ├── tw.png │ │ ├── tz.png │ │ ├── ua.png │ │ ├── ug.png │ │ ├── um.png │ │ ├── us.png │ │ ├── uy.png │ │ ├── uz.png │ │ ├── va.png │ │ ├── vc.png │ │ ├── ve.png │ │ ├── vg.png │ │ ├── vi.png │ │ ├── vn.png │ │ ├── vu.png │ │ ├── wf.png │ │ ├── wo.png │ │ ├── ws.png │ │ ├── ye.png │ │ ├── yt.png │ │ ├── za.png │ │ ├── zm.png │ │ └── zw.png │ ├── award_star_gold_3.png │ ├── award_star_bronze_3.png │ ├── award_star_silver_3.png │ └── flag_white.svg ├── index.php ├── .htaccess ├── actions │ ├── logout.php │ ├── two_factor_auth.php │ ├── login.php │ ├── interest.php │ ├── register.php │ └── recruit.php ├── two_factor_auth.php ├── admin │ ├── new_news.php │ ├── test_restrict_email.php │ ├── edit_exceptions.php │ ├── actions │ │ ├── test_restrict_email.php │ │ ├── search.php │ │ ├── edit_exceptions.php │ │ ├── new_user_type.php │ │ ├── new_dynamic_page.php │ │ ├── new_news.php │ │ ├── new_hint.php │ │ ├── new_restrict_email.php │ │ ├── new_dynamic_menu_item.php │ │ ├── new_category.php │ │ ├── new_email.php │ │ ├── edit_user_type.php │ │ ├── edit_news.php │ │ ├── edit_restrict_email.php │ │ ├── edit_dynamic_page.php │ │ ├── edit_dynamic_menu_item.php │ │ ├── edit_category.php │ │ ├── edit_hint.php │ │ ├── new_challenge.php │ │ ├── list_submissions.php │ │ ├── edit_user.php │ │ └── edit_challenge.php │ ├── new_dynamic_page.php │ ├── search.php │ ├── new_category.php │ ├── new_user_type.php │ ├── list_news.php │ ├── new_hint.php │ ├── new_dynamic_menu_item.php │ ├── new_restrict_email.php │ ├── list_user_types.php │ ├── edit_news.php │ ├── edit_dynamic_page.php │ ├── list_hints.php │ ├── edit_restrict_email.php │ ├── edit_user_type.php │ ├── list_dynamic_pages.php │ ├── edit_hint.php │ ├── new_email.php │ ├── user.php │ ├── edit_category.php │ ├── edit_dynamic_menu_item.php │ ├── new_challenge.php │ ├── edit_user.php │ ├── list_dynamic_menu.php │ ├── list_restrict_email.php │ ├── list_ip_log.php │ └── list_exceptions.php ├── json.php ├── home.php ├── interest.php ├── recruit.php ├── download.php ├── content.php ├── user.php ├── country.php ├── hints.php ├── reset_password.php ├── profile.php └── register.php ├── writable ├── cache │ └── .gitignore └── upload │ └── .gitignore ├── .gitignore ├── .travis.yml ├── install ├── caddy │ ├── CaddyFile │ └── README.md ├── README.md ├── lamp │ ├── mellivora.apache.conf │ └── mellivora.nginx.conf └── docker │ └── README.md ├── .sonarcloud.properties ├── codeception.yml ├── Dockerfile ├── composer.json ├── docker-compose.dev.yml ├── docker-compose.test.yml └── README.md /include/thirdparty/composer/.gitignore: -------------------------------------------------------------------------------- 1 | vendor -------------------------------------------------------------------------------- /tests/codeception/_data/sql/.gitignore: -------------------------------------------------------------------------------- 1 | dump.sql -------------------------------------------------------------------------------- /include/config/.gitignore: -------------------------------------------------------------------------------- 1 | config.inc.php 2 | db.inc.php -------------------------------------------------------------------------------- /tests/codeception/_output/.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | !.gitignore -------------------------------------------------------------------------------- /tests/codeception/_bootstrap.php: -------------------------------------------------------------------------------- 1 | logInAsAnAdmin(); 6 | } 7 | } -------------------------------------------------------------------------------- /tests/codeception/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit (internal) tests. 4 | 5 | class_name: UnitTester 6 | modules: 7 | enabled: 8 | - Asserts 9 | - \Helper\Unit -------------------------------------------------------------------------------- /tests/codeception/_support/Helper/Unit.php: -------------------------------------------------------------------------------- 1 | register($email, $password); 10 | $I->logInAsANormalUser($email, $password); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/codeception/functional.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for functional (integration) tests 4 | # Emulate web requests and make application process them 5 | # Include one of framework modules (Symfony2, Yii2, Laravel5) to use it 6 | 7 | class_name: FunctionalTester 8 | modules: 9 | enabled: 10 | # add framework module here 11 | - \Helper\Functional -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | paths: 3 | tests: tests/codeception 4 | log: tests/codeception/_output 5 | data: tests/codeception/_data 6 | support: tests/codeception/_support 7 | envs: tests/codeception/_envs 8 | settings: 9 | bootstrap: _bootstrap.php 10 | colors: true 11 | memory_limit: 1024M 12 | extensions: 13 | enabled: 14 | - Codeception\Extension\RunFailed -------------------------------------------------------------------------------- /htdocs/img/flag_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /htdocs/two_factor_auth.php: -------------------------------------------------------------------------------- 1 | 'off', 'autofocus'=>true)); 12 | form_hidden('action', 'authenticate'); 13 | form_button_submit(lang_get('authenticate')); 14 | form_end(); 15 | 16 | foot(); -------------------------------------------------------------------------------- /htdocs/actions/two_factor_auth.php: -------------------------------------------------------------------------------- 1 | 'users','name'=>'Users'); 16 | $opts[] = array('id'=>'ip_log','name'=>'IP log'); 17 | 18 | form_select($opts, 'Search in', 'id', 'users', 'name'); 19 | form_button_submit('Search'); 20 | form_xsrf_token(); 21 | form_end(); 22 | 23 | foot(); -------------------------------------------------------------------------------- /htdocs/json.php: -------------------------------------------------------------------------------- 1 | '; 7 | } 8 | 9 | function validate_xsrf_token($token) { 10 | if ($_SESSION[CONST_XSRF_TOKEN_KEY] != $token) { 11 | log_exception(new Exception('Invalid XSRF token. Was: "' . $token.'". Wanted: "' . $_SESSION[CONST_XSRF_TOKEN_KEY].'"')); 12 | message_error('XSRF token mismatch'); 13 | exit; 14 | } 15 | } 16 | 17 | function regenerate_xsrf_token() { 18 | $_SESSION[CONST_XSRF_TOKEN_KEY] = generate_random_string(64); 19 | } -------------------------------------------------------------------------------- /htdocs/admin/new_user_type.php: -------------------------------------------------------------------------------- 1 | 1 21 | ) 22 | ); 23 | 24 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_exceptions.php?generic_success=1'); 25 | } 26 | } -------------------------------------------------------------------------------- /tests/codeception/_data/sql/acceptance/categories.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO categories ( 2 | id, 3 | added, 4 | added_by, 5 | title, 6 | description, 7 | exposed, 8 | available_from, 9 | available_until 10 | ) VALUES ( 11 | 1, 12 | UNIX_TIMESTAMP(), 13 | 1, 14 | 'Default CI Category', 15 | 'This is the default CI category', 16 | 1, 17 | 1451635200, 18 | 4070937600 19 | ); 20 | 21 | INSERT INTO categories ( 22 | id, 23 | added, 24 | added_by, 25 | title, 26 | description, 27 | exposed, 28 | available_from, 29 | available_until 30 | ) VALUES ( 31 | 2, 32 | UNIX_TIMESTAMP(), 33 | 1, 34 | 'Editable CI Category', 35 | 'This is the editable CI category', 36 | 1, 37 | 1451635200, 38 | 4070937600 39 | ); -------------------------------------------------------------------------------- /include/Config.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | ServerAdmin contact@yourdomain.com 4 | ServerName ctf.yourdomain.com 5 | DocumentRoot /var/www/mellivora/htdocs/ 6 | 7 | 8 | Order Deny,Allow 9 | Deny from all 10 | AllowOverride None 11 | 12 | 13 | 14 | Options -Indexes +MultiViews 15 | AllowOverride None 16 | Order Deny,Allow 17 | Require all granted 18 | AddType application/x-httpd-php .php 19 | 20 | 21 | # error log 22 | ErrorLog /var/log/apache2/mellivora-error.log 23 | LogLevel warn 24 | 25 | # access log 26 | CustomLog /var/log/apache2/mellivora-access.log combined 27 | 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7-apache 2 | 3 | MAINTAINER Nakiami 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | git \ 7 | libssl-dev \ 8 | libcurl4-openssl-dev \ 9 | pkg-config \ 10 | zip \ 11 | unzip \ 12 | libonig-dev 13 | 14 | RUN docker-php-ext-install mbstring curl pdo pdo_mysql 15 | 16 | COPY . /var/www/mellivora 17 | COPY install/lamp/mellivora.apache.conf /etc/apache2/sites-available/000-default.conf 18 | 19 | ENV COMPOSER_ALLOW_SUPERUSER=1 20 | RUN curl -sS https://getcomposer.org/installer | php 21 | RUN mv composer.phar /usr/local/bin/composer 22 | WORKDIR /var/www/mellivora/ 23 | RUN composer install --no-dev --optimize-autoloader 24 | 25 | RUN chown -R www-data:www-data /var/www/mellivora 26 | RUN a2enmod rewrite 27 | -------------------------------------------------------------------------------- /htdocs/actions/login.php: -------------------------------------------------------------------------------- 1 | $_POST['title'], 17 | 'description'=>$_POST['description'] 18 | ) 19 | ); 20 | 21 | if ($id) { 22 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_user_type.php?id='.$id); 23 | } else { 24 | message_error('Could not insert new user type.'); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/codeception/acceptance.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for acceptance tests. 4 | # Perform tests in browser using the WebDriver or PhpBrowser. 5 | # If you need both WebDriver and PHPBrowser tests - create a separate suite. 6 | 7 | class_name: AcceptanceTester 8 | modules: 9 | enabled: 10 | - WebDriver: 11 | url: http://mellivora-test/ 12 | browser: chrome 13 | host: chrome 14 | - Db: 15 | dsn: 'mysql:host=db-test;port=3306;dbname=mellivora' 16 | user: 'root' 17 | password: 'password' 18 | dump: 'tests/codeception/_data/sql/dump.sql' 19 | populate: true 20 | cleanup: false 21 | wait: 5 22 | - \Helper\Acceptance -------------------------------------------------------------------------------- /htdocs/admin/actions/new_dynamic_page.php: -------------------------------------------------------------------------------- 1 | $_POST['title'], 17 | 'body'=>$_POST['body'], 18 | 'visibility'=>$_POST['visibility'], 19 | 'min_user_class'=>$_POST['min_user_class'] 20 | ) 21 | ); 22 | 23 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_dynamic_page.php?id='.$id.'&generic_success=1'); 24 | } 25 | } -------------------------------------------------------------------------------- /htdocs/home.php: -------------------------------------------------------------------------------- 1 | '; 17 | section_head($item['title']); 18 | echo ' 19 |
20 | ',get_bbcode()->parse($item['body']),' 21 |
22 | 23 | '; 24 | } 25 | 26 | cache_end(CONST_CACHE_NAME_HOME); 27 | } 28 | 29 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/list_news.php: -------------------------------------------------------------------------------- 1 | '; 15 | section_head(htmlspecialchars($item['title']) . ' Edit', '', false); 16 | echo ' 17 |
18 | ',get_bbcode()->parse($item['body']),' 19 |
20 | 21 | '; 22 | } 23 | 24 | foot(); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nakiami/mellivora", 3 | "type": "project", 4 | "description": "Mellivora is a CTF engine written in PHP", 5 | "homepage": "https://github.com/Nakiami/mellivora", 6 | "license": "GPL-3.0", 7 | "minimum-stability": "stable", 8 | "authors": [ 9 | { 10 | "name": "Nakiami", 11 | "homepage": "https://github.com/Nakiami/" 12 | } 13 | ], 14 | "config": { 15 | "vendor-dir": "include/thirdparty/composer/vendor" 16 | }, 17 | "require": { 18 | "ircmaxell/password-compat": "dev-master", 19 | "ircmaxell/random-lib": "1.2.*", 20 | "google/recaptcha": "~1.1", 21 | "phpmailer/phpmailer": "6.*", 22 | "pear/cache_lite": "~1.8", 23 | "aws/aws-sdk-php": "3.*" 24 | }, 25 | "require-dev": { 26 | "codeception/codeception": "^4.0" 27 | } 28 | } -------------------------------------------------------------------------------- /htdocs/interest.php: -------------------------------------------------------------------------------- 1 | 15 | '; 16 | 17 | if (Config::get('MELLIVORA_CONFIG_RECAPTCHA_ENABLE_PUBLIC')) { 18 | display_captcha(); 19 | } 20 | 21 | form_hidden('action', 'register'); 22 | echo ' 23 | 24 | '; 25 | form_end(); 26 | 27 | foot(); -------------------------------------------------------------------------------- /include/raceconditions.inc.php: -------------------------------------------------------------------------------- 1 | '; 12 | } 13 | 14 | function validate_submission_token($token) { 15 | if ($token != $_SESSION[CONST_SUBMISSION_TOKEN_KEY]) { 16 | message_error('Submission token has expired, please resubmit form'); 17 | } 18 | 19 | regenerate_submission_token(); 20 | } 21 | 22 | function regenerate_submission_token() { 23 | $_SESSION[CONST_SUBMISSION_TOKEN_KEY] = generate_random_string(64); 24 | } -------------------------------------------------------------------------------- /htdocs/admin/new_hint.php: -------------------------------------------------------------------------------- 1 | ${DUMP_FILE} << EOL 18 | /* This file was automatically generated by the test runner */ 19 | 20 | EOL 21 | 22 | # setup 23 | cat ${CODECEPTION_DIR}/_data/sql/setup/*.sql >> ${DUMP_FILE} 24 | 25 | # db structure 26 | cat "install/sql/001-mellivora.sql" >> ${DUMP_FILE}; 27 | 28 | # countries 29 | cat "install/sql/002-countries.sql" >> ${DUMP_FILE}; 30 | 31 | cat ${CODECEPTION_DIR}/_data/sql/acceptance/*.sql >> ${DUMP_FILE} 32 | 33 | cat << EOF 34 | Finished building SQL dump. 35 | 36 | EOF -------------------------------------------------------------------------------- /include/config/db.default.inc.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | '; 20 | 21 | country_select(); 22 | 23 | form_hidden('action', 'register'); 24 | echo ' 25 | 26 | '; 27 | form_end(); 28 | 29 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/actions/new_news.php: -------------------------------------------------------------------------------- 1 | time(), 19 | 'added_by'=>$_SESSION['id'], 20 | 'title'=>$_POST['title'], 21 | 'body'=>$_POST['body'] 22 | ) 23 | ); 24 | 25 | if ($id) { 26 | invalidate_cache(CONST_CACHE_NAME_HOME); 27 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_news.php?id='.$id); 28 | } else { 29 | message_error('Could not insert new news item.'); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /htdocs/admin/actions/new_hint.php: -------------------------------------------------------------------------------- 1 | time(), 17 | 'added_by'=>$_SESSION['id'], 18 | 'challenge'=>$_POST['challenge'], 19 | 'visible'=>$_POST['visible'], 20 | 'body'=>$_POST['body'] 21 | ) 22 | ); 23 | 24 | if ($id) { 25 | invalidate_cache(CONST_CACHE_NAME_HINTS); 26 | 27 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_hint.php?id='.$id); 28 | } else { 29 | message_error('Could not insert new hint.'); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /htdocs/admin/new_dynamic_menu_item.php: -------------------------------------------------------------------------------- 1 | 0,'title'=>'--- No internal link ---')); 26 | form_select($pages, 'Internal page', 'id', null, 'title'); 27 | 28 | user_class_select(); 29 | 30 | form_input_text('URL'); 31 | 32 | form_input_text('Priority'); 33 | 34 | form_hidden('action', 'new'); 35 | form_button_submit('Create'); 36 | form_end(); 37 | 38 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/actions/new_restrict_email.php: -------------------------------------------------------------------------------- 1 | time(), 17 | 'added_by'=>$_SESSION['id'], 18 | 'rule'=>$_POST['rule'], 19 | 'white'=>$_POST['whitelist'], 20 | 'priority'=>$_POST['priority'], 21 | 'enabled'=>$_POST['enabled'] 22 | ) 23 | ); 24 | 25 | if ($id) { 26 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_restrict_email.php?generic_success=1'); 27 | } else { 28 | message_error('Could not insert new email restriction.'); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /htdocs/admin/new_restrict_email.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | Title 16 | Description 17 | 18 | 19 | 20 | 21 | '; 22 | 23 | $types = db_query_fetch_all('SELECT * FROM user_types ORDER BY title ASC'); 24 | 25 | foreach($types as $type) { 26 | echo ' 27 | 28 | ',htmlspecialchars($type['title']),' 29 | ',short_description($type['description'], 50),' 30 | Edit 31 | 32 | '; 33 | } 34 | 35 | echo ' 36 | 37 | 38 | '; 39 | 40 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/actions/new_dynamic_menu_item.php: -------------------------------------------------------------------------------- 1 | $_POST['title'], 17 | 'permalink'=>$_POST['permalink'], 18 | 'url'=>$_POST['url'], 19 | 'visibility'=>$_POST['visibility'], 20 | 'min_user_class'=>$_POST['min_user_class'], 21 | 'priority'=>$_POST['priority'], 22 | 'internal_page'=>$_POST['internal_page'] 23 | ) 24 | ); 25 | 26 | invalidate_cache_group(CONST_CACHE_GROUP_NAME_DYNAMIC_MENU); 27 | 28 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_dynamic_menu_item.php?id='.$id.'&generic_success=1'); 29 | } 30 | } -------------------------------------------------------------------------------- /tests/codeception/unit/GeneralTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('aaa', cut_string('aaaaaa', 3)); 7 | } 8 | 9 | public function test_cut_string_sameLength() { 10 | $this->assertEquals('aaa', cut_string('aaa', 3)); 11 | } 12 | 13 | public function test_cut_string_empty() { 14 | $this->assertEquals('', cut_string('', 3)); 15 | } 16 | 17 | public function test_short_description() { 18 | $this->assertEquals('aa ...', short_description('aaaa', 2)); 19 | } 20 | 21 | public function test_short_description_noCut() { 22 | $this->assertEquals('aa', short_description('aa', 2)); 23 | } 24 | 25 | public function test_permalink() { 26 | $string = ' This Is A permalink!! &&?? ## alright!'; 27 | $expected = 'this-is-a-permalink-alright'; 28 | 29 | $this->assertEquals( 30 | to_permalink($string), 31 | $expected 32 | ); 33 | } 34 | } -------------------------------------------------------------------------------- /htdocs/admin/edit_news.php: -------------------------------------------------------------------------------- 1 | $_GET['id']) 13 | ); 14 | 15 | head('Site management'); 16 | menu_management(); 17 | 18 | section_subhead('Edit news item: ' . $news['title']); 19 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_news'); 20 | form_input_text('Title', $news['title']); 21 | form_textarea('Body', $news['body']); 22 | form_hidden('action', 'edit'); 23 | form_hidden('id', $_GET['id']); 24 | form_button_submit('Save changes'); 25 | form_bbcode_manual(); 26 | form_end(); 27 | 28 | section_subhead('Delete news item'); 29 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_news'); 30 | form_input_checkbox('Delete confirmation'); 31 | form_hidden('action', 'delete'); 32 | form_hidden('id', $_GET['id']); 33 | form_button_submit('Delete news item', 'danger'); 34 | form_end(); 35 | 36 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/actions/new_category.php: -------------------------------------------------------------------------------- 1 | time(), 19 | 'added_by'=>$_SESSION['id'], 20 | 'title'=>$_POST['title'], 21 | 'description'=>$_POST['description'], 22 | 'exposed'=>$_POST['exposed'], 23 | 'available_from'=>strtotime($_POST['available_from']), 24 | 'available_until'=>strtotime($_POST['available_until']) 25 | ) 26 | ); 27 | 28 | if ($id) { 29 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_category.php?id='.$id); 30 | } else { 31 | message_error('Could not insert new category.'); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /include/captcha.inc.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | '; 8 | } 9 | 10 | function validate_captcha () { 11 | try { 12 | $captcha = new \ReCaptcha\ReCaptcha( 13 | Config::get('MELLIVORA_CONFIG_RECAPTCHA_PRIVATE_KEY'), 14 | new \ReCaptcha\RequestMethod\CurlPost() 15 | ); 16 | 17 | $response = $captcha->verify( 18 | $_POST['g-recaptcha-response'], 19 | get_ip() 20 | ); 21 | 22 | if (!$response->isSuccess()) { 23 | message_error('Captcha error'); 24 | } 25 | 26 | } catch (Exception $e) { 27 | log_exception($e); 28 | message_error('Caught exception processing captcha. Please contact '.(Config::get('MELLIVORA_CONFIG_EMAIL_REPLYTO_EMAIL') ? Config::get('MELLIVORA_CONFIG_EMAIL_REPLYTO_EMAIL') : Config::get('MELLIVORA_CONFIG_EMAIL_FROM_EMAIL'))); 29 | } 30 | } -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | Testing 2 | ========= 3 | 4 | Mellivora is tested using [Codeception](http://codeception.com/). Builds on [TravisCI](https://travis-ci.org/Nakiami/mellivora) are used to verify before merging. 5 | 6 | ### Requirements 7 | 8 | * [Docker](https://docs.docker.com/) and [docker-compose](https://docs.docker.com/compose/) 9 | 10 | ### Running tests locally 11 | 12 | Running ``./tests/run_tests`` should do the trick. 13 | 14 | If you are making changes to composer requirements, you will need to delete/rebuild the docker image ``composerdependencies``. 15 | You can do this by calling ``docker-compose -f docker-compose.test.yml build``. 16 | 17 | ### Tips 18 | 19 | - Call ``docker-compose -f docker-compose.test.yml build`` to rebuild all containers. 20 | - HTML output and screenshots of failing acceptance tests can be found in ``tests/codeception/_output/``. 21 | - Your local settings in ``include/config/config.inc.php`` are used when running tests. Some settings will cause tests to fail, like enabling caching, captcha, etc. It is recommended to remove any non-default configuration while running acceptance tests locally. 22 | -------------------------------------------------------------------------------- /tests/codeception/_data/sql/acceptance/users.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO 2 | users ( 3 | id, 4 | email, 5 | team_name, 6 | added, 7 | passhash, 8 | download_key, 9 | class, 10 | enabled, 11 | competing, 12 | country_id 13 | ) VALUES ( 14 | 1, 15 | 'ci-admin@mellivora.co', 16 | 'ci-admin', 17 | UNIX_TIMESTAMP(), 18 | '$2y$10$dvjRH6GA4B4owKhXMWobKOfbqT48HH0SKSwsL0c9ckUXTSSfaq1l2', 19 | 'a4c62dcecca552be3890df0c56603a810a9c8a0081f0e9993ae9c53e344e81d4', 20 | 100, -- class 21 | 1, -- enabled 22 | 0, -- competing 23 | 1 -- country_id 24 | ); 25 | 26 | INSERT INTO 27 | users ( 28 | email, 29 | team_name, 30 | added, 31 | passhash, 32 | download_key, 33 | class, 34 | enabled, 35 | competing, 36 | country_id 37 | ) VALUES ( 38 | 'competitor@mellivora.co', 39 | 'competitor', 40 | UNIX_TIMESTAMP(), 41 | '$2y$10$dvjRH6GA4B4owKhXMWobKOfbqT48HH0SKSwsL0c9ckUXTSSfaq1l2', 42 | 'b5d62dcecca552be3890df0c56603a810a9c8a0081f0e9993ae9c53e344e82e5', 43 | 0, -- class 44 | 1, -- enabled 45 | 1, -- competing 46 | 1 -- country_id 47 | ); -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mellivora: 4 | image: mellivora 5 | ports: 6 | - 80:80 7 | - 443:443 8 | build: 9 | context: . 10 | dockerfile: Dockerfile 11 | environment: 12 | MELLIVORA_CONFIG_DB_ENGINE: mysql 13 | MELLIVORA_CONFIG_DB_HOST: db 14 | MELLIVORA_CONFIG_DB_PORT: 3306 15 | MELLIVORA_CONFIG_DB_NAME: mellivora 16 | MELLIVORA_CONFIG_DB_USER: meldbuser 17 | MELLIVORA_CONFIG_DB_PASSWORD: password 18 | volumes: 19 | - .:/var/www/mellivora 20 | - composerdependencies:/var/www/mellivora/include/thirdparty/composer 21 | links: 22 | - db 23 | db: 24 | image: mysql:8 25 | ports: 26 | - 13306:3306 27 | environment: 28 | MYSQL_DATABASE: mellivora 29 | MYSQL_USER: meldbuser 30 | MYSQL_PASSWORD: password 31 | MYSQL_ROOT_PASSWORD: password 32 | volumes: 33 | - dbdata:/var/lib/mysql 34 | - ./install/sql:/docker-entrypoint-initdb.d 35 | adminer: 36 | image: adminer 37 | restart: always 38 | ports: 39 | - 18080:8080 40 | volumes: 41 | composerdependencies: 42 | dbdata: 43 | -------------------------------------------------------------------------------- /htdocs/admin/actions/new_email.php: -------------------------------------------------------------------------------- 1 | $_GET['team_key']) 9 | ); 10 | 11 | if (!is_valid_id($user['id'])) { 12 | log_exception(new Exception('Invalid team key used for download')); 13 | message_error(lang_get('invalid_team_key')); 14 | } 15 | 16 | if (!$user['enabled']) { 17 | message_error(lang_get('user_not_enabled')); 18 | } 19 | 20 | $file = db_query_fetch_one(' 21 | SELECT 22 | f.id, 23 | f.title, 24 | f.size, 25 | f.md5, 26 | c.available_from 27 | FROM files AS f 28 | LEFT JOIN challenges AS c ON c.id = f.challenge 29 | WHERE f.download_key = :download_key', 30 | array( 31 | 'download_key'=>$_GET['file_key'] 32 | ) 33 | ); 34 | 35 | if (!is_valid_id($file['id'])) { 36 | log_exception(new Exception('Invalid file key used for download')); 37 | message_error(lang_get('no_file_found')); 38 | } 39 | 40 | if (time() < $file['available_from'] && !user_is_staff()) { 41 | message_error(lang_get('file_not_available')); 42 | } 43 | 44 | download_file($file); -------------------------------------------------------------------------------- /include/language/language.inc.php: -------------------------------------------------------------------------------- 1 | $_GET['id']) 13 | ); 14 | 15 | head('Site management'); 16 | menu_management(); 17 | 18 | section_subhead('Edit dynamic page: ' . $page['title']); 19 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_dynamic_page'); 20 | form_input_text('Title', $page['title']); 21 | form_textarea('Body', $page['body']); 22 | 23 | dynamic_visibility_select($page['visibility']); 24 | 25 | user_class_select($page['min_user_class']); 26 | 27 | form_hidden('action', 'edit'); 28 | form_hidden('id', $_GET['id']); 29 | 30 | form_button_submit('Save changes'); 31 | form_bbcode_manual(); 32 | form_end(); 33 | 34 | section_subhead('Delete'); 35 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_dynamic_page'); 36 | form_input_checkbox('Delete confirmation'); 37 | form_hidden('action', 'delete'); 38 | form_hidden('id', $_GET['id']); 39 | form_button_submit('Delete', 'danger'); 40 | form_end(); 41 | 42 | foot(); -------------------------------------------------------------------------------- /include/mellivora.inc.php: -------------------------------------------------------------------------------- 1 | $_POST['title'], 18 | 'description'=>$_POST['description'] 19 | ), 20 | array( 21 | 'id'=>$_POST['id'] 22 | ) 23 | ); 24 | 25 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_user_types.php?generic_success=1'); 26 | } 27 | 28 | else if ($_POST['action'] == 'delete') { 29 | 30 | if (!$_POST['delete_confirmation']) { 31 | message_error('Please confirm delete'); 32 | } 33 | 34 | db_delete( 35 | 'user_types', 36 | array( 37 | 'id'=>$_POST['id'] 38 | ) 39 | ); 40 | 41 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_user_types.php?generic_success=1'); 42 | } 43 | } -------------------------------------------------------------------------------- /htdocs/admin/list_hints.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | Challenge 16 | Added 17 | Hint 18 | Manage 19 | 20 | 21 | 22 | '; 23 | 24 | $hints = db_query_fetch_all(' 25 | SELECT 26 | h.id, 27 | h.added, 28 | h.body, 29 | c.title 30 | FROM hints AS h 31 | LEFT JOIN challenges AS c ON c.id = h.challenge' 32 | ); 33 | 34 | foreach($hints as $hint) { 35 | echo ' 36 | 37 | ',htmlspecialchars($hint['title']),' 38 | ',date_time($hint['added']),' 39 | ',htmlspecialchars($hint['body']), ' 40 | Edit 41 | 42 | '; 43 | } 44 | 45 | echo ' 46 | 47 | 48 | '; 49 | 50 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/edit_restrict_email.php: -------------------------------------------------------------------------------- 1 | $_GET['id']) 18 | ); 19 | 20 | head('Site management'); 21 | menu_management(); 22 | 23 | section_subhead('Edit signup rule'); 24 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_restrict_email'); 25 | form_input_text('Rule', $rule['rule']); 26 | form_input_text('Priority', $rule['priority']); 27 | form_input_checkbox('Whitelist', $rule['white']); 28 | form_input_checkbox('Enabled', $rule['enabled']); 29 | form_hidden('action', 'edit'); 30 | form_hidden('id', $_GET['id']); 31 | form_button_submit('Save changes'); 32 | form_end(); 33 | 34 | section_subhead('Delete rule'); 35 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_restrict_email'); 36 | form_input_checkbox('Delete confirmation'); 37 | form_hidden('action', 'delete'); 38 | form_hidden('id', $_GET['id']); 39 | form_button_submit('Delete rule', 'danger'); 40 | form_end(); 41 | 42 | foot(); -------------------------------------------------------------------------------- /htdocs/content.php: -------------------------------------------------------------------------------- 1 | $_GET['show'] 18 | ) 19 | ); 20 | 21 | if (!is_valid_id($menu_data['internal_page'])) { 22 | message_error(lang_get('not_a_valid_link')); 23 | } 24 | 25 | $content = db_select_one( 26 | 'dynamic_pages', 27 | array( 28 | 'id', 29 | 'title', 30 | 'body', 31 | 'visibility', 32 | 'min_user_class' 33 | ), 34 | array( 35 | 'id'=>$menu_data['internal_page'] 36 | ) 37 | ); 38 | 39 | if ($content['visibility'] == 'private') { 40 | enforce_authentication($content['min_user_class']); 41 | } 42 | 43 | head($content['title']); 44 | 45 | if (cache_start($content['id'], Config::get('MELLIVORA_CONFIG_CACHE_TIME_DYNAMIC'), CONST_CACHE_DYNAMIC_PAGES_GROUP)) { 46 | 47 | section_head($content['title']); 48 | 49 | echo get_bbcode()->parse($content['body']); 50 | 51 | cache_end($content['id'], CONST_CACHE_DYNAMIC_PAGES_GROUP); 52 | } 53 | 54 | foot(); -------------------------------------------------------------------------------- /htdocs/user.php: -------------------------------------------------------------------------------- 1 | $_GET['id']) 22 | ); 23 | 24 | if (empty($user)) { 25 | message_generic( 26 | lang_get('sorry'), 27 | lang_get('no_user_found'), 28 | false 29 | ); 30 | } 31 | 32 | section_head(htmlspecialchars($user['team_name']), country_flag_link($user['country_name'], $user['country_code'], true), false); 33 | 34 | if (!$user['competing']) { 35 | message_inline_blue(lang_get('non_competing_user')); 36 | } 37 | 38 | print_solved_graph($_GET['id']); 39 | 40 | print_solved_challenges($_GET['id']); 41 | 42 | cache_end(CONST_CACHE_NAME_USER . $_GET['id']); 43 | } 44 | 45 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/edit_user_type.php: -------------------------------------------------------------------------------- 1 | $_GET['id']) 17 | ); 18 | 19 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_user_type'); 20 | form_input_text('Title', $user_type['title']); 21 | form_textarea('Description', $user_type['description']); 22 | form_hidden('action', 'edit'); 23 | form_hidden('id', $_GET['id']); 24 | form_button_submit('Save changes'); 25 | form_end(); 26 | 27 | section_subhead('Delete user type'); 28 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_user_type'); 29 | form_input_checkbox('Delete confirmation'); 30 | form_hidden('action', 'delete'); 31 | form_hidden('id', $_GET['id']); 32 | message_inline_red('Warning! Any users of this type will be without a type. 33 | You must manually give them a type in the DB. If no types will exist after this action, you must set their type to 0.'); 34 | form_button_submit('Delete user type', 'danger'); 35 | form_end(); 36 | 37 | foot(); -------------------------------------------------------------------------------- /htdocs/actions/interest.php: -------------------------------------------------------------------------------- 1 | $_POST['email']) 20 | ); 21 | 22 | if ($interest['id']) { 23 | message_error('You have already registered your interest!'); 24 | } 25 | 26 | $id = db_insert( 27 | 'interest', 28 | array( 29 | 'added'=>time(), 30 | 'name'=>$_POST['name'], 31 | 'email'=>$_POST['email'], 32 | 'secret'=>generate_random_string(40) 33 | ) 34 | ); 35 | 36 | if ($id) { 37 | message_generic('Success', 'The email '.htmlspecialchars($_POST['email']).' has been registered. We look forward to seeing you in our next competition!'); 38 | } else { 39 | message_error('Could not register interest. You must not be interested enough!'); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /include/layout/dynamic.inc.php: -------------------------------------------------------------------------------- 1 | 27 | ',htmlspecialchars($entry['title']),' 28 | 29 | '; 30 | } 31 | 32 | cache_end($cache_name, CONST_CACHE_DYNAMIC_MENU_GROUP); 33 | } 34 | } -------------------------------------------------------------------------------- /htdocs/admin/actions/edit_news.php: -------------------------------------------------------------------------------- 1 | $_POST['title'], 18 | 'body'=>$_POST['body'] 19 | ), 20 | array( 21 | 'id'=>$_POST['id'] 22 | ) 23 | ); 24 | 25 | invalidate_cache(CONST_CACHE_NAME_HOME); 26 | 27 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_news.php?id='.$_POST['id'].'&generic_success=1'); 28 | } 29 | 30 | else if ($_POST['action'] == 'delete') { 31 | 32 | if (!$_POST['delete_confirmation']) { 33 | message_error('Please confirm delete'); 34 | } 35 | 36 | db_delete( 37 | 'news', 38 | array( 39 | 'id'=>$_POST['id'] 40 | ) 41 | ); 42 | 43 | invalidate_cache(CONST_CACHE_NAME_HOME); 44 | 45 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_news.php?generic_success=1'); 46 | } 47 | } -------------------------------------------------------------------------------- /htdocs/admin/actions/edit_restrict_email.php: -------------------------------------------------------------------------------- 1 | $_POST['rule'], 18 | 'enabled'=>$_POST['enabled'], 19 | 'white'=>$_POST['whitelist'], 20 | 'priority'=>$_POST['priority'] 21 | ), 22 | array( 23 | 'id'=>$_POST['id'] 24 | ) 25 | ); 26 | 27 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_restrict_email.php?generic_success=1'); 28 | } 29 | 30 | else if ($_POST['action'] == 'delete') { 31 | 32 | if (!$_POST['delete_confirmation']) { 33 | message_error('Please confirm delete'); 34 | } 35 | 36 | db_delete( 37 | 'restrict_email', 38 | array( 39 | 'id'=>$_POST['id'] 40 | ) 41 | ); 42 | 43 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_restrict_email.php?generic_success=1'); 44 | } 45 | } -------------------------------------------------------------------------------- /tests/codeception/acceptance/SubmitFlagCest.php: -------------------------------------------------------------------------------- 1 | logInAsANormalUser(); 6 | 7 | $I->amOnPage('/challenges?category=' . to_permalink(CI_DEFAULT_CATEGORY_TITLE)); 8 | $I->see(CI_DEFAULT_CHALLENGE_TITLE); 9 | $I->see(CI_DEFAULT_CHALLENGE_DESCRIPTION); 10 | 11 | $flag_field = '#flag-input-' . CI_DEFAULT_CHALLENGE_ID; 12 | $I->fillField($flag_field, 'NOT_THE_FLAG'); 13 | $I->click('#flag-submit-' . CI_DEFAULT_CHALLENGE_ID); 14 | 15 | $I->seeInCurrentUrl('status=incorrect'); 16 | $I->seeElement($flag_field); 17 | } 18 | 19 | public function shouldBeAbleToSubmitACorrectFlag(AcceptanceTester $I) { 20 | $I->logInAsANormalUser(); 21 | 22 | $I->amOnPage('/challenges?category=' . to_permalink(CI_DEFAULT_CATEGORY_TITLE)); 23 | $I->see(CI_DEFAULT_CHALLENGE_TITLE); 24 | $I->see(CI_DEFAULT_CHALLENGE_DESCRIPTION); 25 | 26 | $flag_field = '#flag-input-' . CI_DEFAULT_CHALLENGE_ID; 27 | $I->fillField($flag_field, CI_DEFAULT_CHALLENGE_FLAG); 28 | $I->click('#flag-submit-' . CI_DEFAULT_CHALLENGE_ID); 29 | 30 | $I->seeInCurrentUrl('status=correct'); 31 | $I->dontSeeElement($flag_field); 32 | } 33 | } -------------------------------------------------------------------------------- /htdocs/actions/register.php: -------------------------------------------------------------------------------- 1 | $_POST['email']) 20 | ); 21 | 22 | if ($recruit['id']) { 23 | message_generic('Thank you', 'Your email was already registered!'); 24 | } 25 | 26 | $id = db_insert( 27 | 'recruit', 28 | array( 29 | 'added'=>time(), 30 | 'user_id'=>$_SESSION['id'], 31 | 'name'=>$_POST['name'], 32 | 'email'=>$_POST['email'], 33 | 'city'=>$_POST['city'], 34 | 'country'=>$_POST['country'] 35 | ) 36 | ); 37 | 38 | if ($id) { 39 | message_generic('Success', 'The email '.htmlspecialchars($_POST['email']).' has been registered. Thanks!'); 40 | } else { 41 | message_error('Could not register interest. You must not be interested enough!'); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /htdocs/admin/list_dynamic_pages.php: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | Title 29 | visibility 30 | Min user class 31 | Manage 32 | 33 | 34 | 35 | '; 36 | 37 | foreach($pages as $item) { 38 | echo ' 39 | 40 | ',htmlspecialchars($item['title']),' 41 | ',visibility_enum_to_name($item['visibility']), ' 42 | ',user_class_name($item['min_user_class']), ' 43 | Edit 44 | 45 | '; 46 | } 47 | 48 | echo ' 49 | 50 | 51 | '; 52 | 53 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/edit_hint.php: -------------------------------------------------------------------------------- 1 | $_GET['id']) 17 | ); 18 | 19 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_hint'); 20 | form_textarea('Body', $hint['body']); 21 | 22 | $opts = db_query_fetch_all( 23 | 'SELECT 24 | ch.id, 25 | ch.title, 26 | ca.title AS category 27 | FROM challenges AS ch 28 | LEFT JOIN categories AS ca ON ca.id = ch.category 29 | ORDER BY ca.title, ch.title' 30 | ); 31 | 32 | form_select($opts, 'Challenge', 'id', $hint['challenge'], 'title', 'category'); 33 | form_input_checkbox('Visible', $hint['visible']); 34 | form_hidden('action', 'edit'); 35 | form_hidden('id', $_GET['id']); 36 | form_button_submit('Save changes'); 37 | form_end(); 38 | 39 | section_subhead('Delete hint'); 40 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_hint'); 41 | form_input_checkbox('Delete confirmation'); 42 | form_hidden('action', 'delete'); 43 | form_hidden('id', $_GET['id']); 44 | form_button_submit('Delete hint', 'danger'); 45 | form_end(); 46 | 47 | foot(); -------------------------------------------------------------------------------- /install/lamp/mellivora.nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | listen 443 ssl; 5 | 6 | # ======================================================== 7 | # =========== Modify from here =========================== 8 | # ======================================================== 9 | 10 | root /var/www/mellivora/htdocs; 11 | server_name ctf.yourdomain.com; 12 | 13 | index index.html index.htm index.php; 14 | 15 | access_log /var/log/nginx/ctf.yourdomain.com_access.log; 16 | error_log /var/log/nginx/ctf.yourdomain.com_error.log; 17 | 18 | ssl on; 19 | ssl_certificate /etc/nginx/ssl/ctf.yourdomain.com.crt; 20 | ssl_certificate_key /etc/nginx/ssl/ctf.yourdomain.com.key; 21 | 22 | # ======================================================== 23 | # =========== End of modify ============================== 24 | # ======================================================== 25 | 26 | location / { 27 | try_files $uri $uri/ @extensionless-php; 28 | } 29 | 30 | location @extensionless-php { 31 | rewrite ^(.*)$ $1.php last; 32 | } 33 | 34 | location ~ \.php(?:$|/) { 35 | include /etc/nginx/fastcgi_params; 36 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 37 | fastcgi_index index.php; 38 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 39 | fastcgi_param PATH_INFO $fastcgi_path_info; 40 | fastcgi_param HTTPS on; 41 | fastcgi_pass unix:/var/run/php5-fpm.sock; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /htdocs/admin/new_email.php: -------------------------------------------------------------------------------- 1 | htmlspecialchars($message))); 5 | } 6 | 7 | function json_scoreboard ($user_type = null) { 8 | 9 | $values = array(); 10 | 11 | if (is_valid_id($user_type)) { 12 | $values['user_type'] = $user_type; 13 | } 14 | 15 | $scores = db_query_fetch_all(' 16 | SELECT 17 | u.id AS user_id, 18 | u.team_name, 19 | co.country_code, 20 | SUM(c.points) AS score, 21 | MAX(s.added) AS tiebreaker 22 | FROM users AS u 23 | LEFT JOIN countries AS co ON co.id = u.country_id 24 | LEFT JOIN submissions AS s ON u.id = s.user_id AND s.correct = 1 25 | LEFT JOIN challenges AS c ON c.id = s.challenge 26 | WHERE 27 | u.competing = 1 28 | '.(is_valid_id($user_type) ? 'AND u.user_type = :user_type' : '').' 29 | GROUP BY u.id 30 | ORDER BY score DESC, tiebreaker ASC', 31 | $values 32 | ); 33 | 34 | $scoreboard = array(); 35 | for ($i = 0; $i < count($scores); $i++) { 36 | $scoreboard['standings'][$i] = array( 37 | 'pos'=>($i+1), 38 | 'team'=>$scores[$i]['team_name'], 39 | 'score'=>array_get($scores[$i], 'score', 0), 40 | 'country'=>$scores[$i]['country_code'] 41 | ); 42 | } 43 | 44 | echo json_encode($scoreboard); 45 | } -------------------------------------------------------------------------------- /htdocs/admin/user.php: -------------------------------------------------------------------------------- 1 | $_GET['id']) 24 | ); 25 | 26 | if (empty($user)) { 27 | message_generic( 28 | lang_get('sorry'), 29 | lang_get('no_user_found'), 30 | false); 31 | } 32 | 33 | section_head( 34 | htmlspecialchars($user['team_name']), 35 | country_flag_link( 36 | $user['country_name'], $user['country_code'], true) . 37 | button_link('Edit user', 'edit_user?id='.htmlspecialchars($user['id'])) . ' ' . 38 | button_link('Email user', 'new_email?to='.htmlspecialchars($user['email'])), 39 | false 40 | ); 41 | 42 | if (!$user['competing']) { 43 | message_inline_blue(lang_get('non_competing_user')); 44 | } 45 | 46 | print_solved_graph($_GET['id']); 47 | 48 | print_solved_challenges($_GET['id']); 49 | 50 | print_user_ip_log($_GET['id'], 5); 51 | 52 | print_user_submissions($_GET['id'], 5); 53 | 54 | print_user_exception_log($_GET['id'], 5); 55 | 56 | foot(); -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | codecept: 4 | image: eccube/php7-ext-codeception 5 | depends_on: 6 | - chrome 7 | - mellivora-test 8 | - db-test 9 | volumes: 10 | - .:/project 11 | chrome: 12 | image: 'selenium/standalone-chrome-debug' 13 | ports: 14 | - '5900' 15 | - '4444' 16 | dns: 8.8.4.4 17 | environment: 18 | - no_proxy=localhost 19 | mellivora-test: 20 | image: mellivora-test 21 | build: 22 | context: . 23 | dockerfile: Dockerfile 24 | environment: 25 | MELLIVORA_CONFIG_DB_ENGINE: mysql 26 | MELLIVORA_CONFIG_DB_HOST: db-test 27 | MELLIVORA_CONFIG_DB_PORT: 3306 28 | MELLIVORA_CONFIG_DB_NAME: mellivora 29 | MELLIVORA_CONFIG_DB_USER: meldbuser 30 | MELLIVORA_CONFIG_DB_PASSWORD: password 31 | MELLIVORA_CONFIG_SITE_URL: http://mellivora-test/ 32 | MELLIVORA_CONFIG_SITE_URL_STATIC_RESOURCES: http://mellivora-test/ 33 | volumes: 34 | - .:/var/www/mellivora 35 | - composerdependencies:/var/www/mellivora/include/thirdparty/composer 36 | links: 37 | - db-test 38 | db-test: 39 | image: mysql:8 40 | command: --default-authentication-plugin=mysql_native_password 41 | restart: always 42 | environment: 43 | MYSQL_DATABASE: mellivora 44 | MYSQL_USER: meldbuser 45 | MYSQL_PASSWORD: password 46 | MYSQL_ROOT_PASSWORD: password 47 | volumes: 48 | composerdependencies: -------------------------------------------------------------------------------- /include/thirdparty/nbbc/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (C) 2008-10, the Phantom Inker. All rights reserved. 3 | Portions copyright (c) 2004-2008 AddedBytes.com 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in 14 | the documentation and/or other materials provided with the 15 | distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE PHANTOM INKER "AS IS" AND ANY EXPRESS 18 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 24 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 25 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 27 | IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /include/two_factor_auth.inc.php: -------------------------------------------------------------------------------- 1 | $_SESSION['id'] 17 | ) 18 | ); 19 | 20 | if (empty($user['id']) || empty($user['secret'])) { 21 | message_error('No two-factor authentication tokens found for this user.'); 22 | } 23 | 24 | return Google2FA::get_qr_code_url($user['team_name'], $user['secret']); 25 | } 26 | 27 | function validate_two_factor_auth_code($code) { 28 | require_once(CONST_PATH_THIRDPARTY.'Google2FA/Google2FA.php'); 29 | 30 | $valid = false; 31 | 32 | $secret = db_select_one( 33 | 'two_factor_auth', 34 | array( 35 | 'secret' 36 | ), 37 | array( 38 | 'user_id'=>$_SESSION['id'] 39 | ) 40 | ); 41 | 42 | try { 43 | $valid = Google2FA::verify_key($secret['secret'], $code); 44 | } catch (Exception $e) { 45 | message_error('Could not verify key.'); 46 | } 47 | 48 | return $valid; 49 | } 50 | 51 | function generate_two_factor_auth_secret($length) { 52 | return generate_random_string($length, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'); 53 | } -------------------------------------------------------------------------------- /htdocs/admin/actions/edit_dynamic_page.php: -------------------------------------------------------------------------------- 1 | $_POST['title'], 18 | 'body'=>$_POST['body'], 19 | 'visibility'=>$_POST['visibility'], 20 | 'min_user_class'=>$_POST['min_user_class'] 21 | ), 22 | array( 23 | 'id'=>$_POST['id'] 24 | ) 25 | ); 26 | 27 | invalidate_cache($_POST['id'], CONST_CACHE_DYNAMIC_PAGES_GROUP); 28 | 29 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_dynamic_page.php?id='.$_POST['id'].'&generic_success=1'); 30 | } 31 | 32 | else if ($_POST['action'] == 'delete') { 33 | 34 | if (!$_POST['delete_confirmation']) { 35 | message_error('Please confirm delete'); 36 | } 37 | 38 | db_delete( 39 | 'dynamic_pages', 40 | array( 41 | 'id'=>$_POST['id'] 42 | ) 43 | ); 44 | 45 | invalidate_cache($_POST['id'], CONST_CACHE_DYNAMIC_PAGES_GROUP); 46 | 47 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_dynamic_pages.php?generic_success=1'); 48 | } 49 | } -------------------------------------------------------------------------------- /htdocs/admin/edit_category.php: -------------------------------------------------------------------------------- 1 | $_GET['id']) 13 | ); 14 | 15 | if (empty($category)) { 16 | message_error('No category found with this ID'); 17 | } 18 | 19 | head('Site management'); 20 | menu_management(); 21 | 22 | section_subhead('Edit category: ' . $category['title']); 23 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_category'); 24 | form_input_text('Title', $category['title']); 25 | form_textarea('Description', $category['description']); 26 | form_input_checkbox('Exposed', $category['exposed']); 27 | form_input_text('Available from', date_time($category['available_from'])); 28 | form_input_text('Available until', date_time($category['available_until'])); 29 | form_hidden('action', 'edit'); 30 | form_hidden('id', $_GET['id']); 31 | form_button_submit('Save changes'); 32 | form_end(); 33 | 34 | section_subhead('Delete category: ' . $category['title']); 35 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_category'); 36 | form_input_checkbox('Delete confirmation'); 37 | form_hidden('action', 'delete'); 38 | form_hidden('id', $_GET['id']); 39 | message_inline_red('Warning! This will delete all challenges under this category, as well as all submissions, files, and hints related those challenges!'); 40 | form_button_submit('Delete category', 'danger'); 41 | form_end(); 42 | 43 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/edit_dynamic_menu_item.php: -------------------------------------------------------------------------------- 1 | $_GET['id']) 17 | ); 18 | 19 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_dynamic_menu_item'); 20 | 21 | form_input_text('Title', $menu_item['title']); 22 | form_input_text('Permalink', $menu_item['permalink']); 23 | 24 | dynamic_visibility_select($menu_item['visibility']); 25 | 26 | $pages = db_select_all( 27 | 'dynamic_pages', 28 | array( 29 | 'id', 30 | 'title' 31 | ) 32 | ); 33 | array_unshift($pages, array('id'=>0,'title'=>'--- No internal link ---')); 34 | form_select($pages, 'Internal page', 'id', $menu_item['internal_page'], 'title'); 35 | 36 | user_class_select($menu_item['min_user_class']); 37 | 38 | form_input_text('URL', $menu_item['url']); 39 | 40 | form_input_text('Priority', $menu_item['priority']); 41 | 42 | form_hidden('action', 'edit'); 43 | form_hidden('id', $_GET['id']); 44 | form_button_submit('Save changes'); 45 | form_end(); 46 | 47 | section_subhead('Delete menu item'); 48 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_dynamic_menu_item'); 49 | form_input_checkbox('Delete confirmation'); 50 | form_hidden('action', 'delete'); 51 | form_hidden('id', $_GET['id']); 52 | form_button_submit('Delete menu item', 'danger'); 53 | form_end(); 54 | 55 | foot(); 56 | -------------------------------------------------------------------------------- /htdocs/admin/new_challenge.php: -------------------------------------------------------------------------------- 1 | 0, 'title'=> '-- This challenge will become available after the selected challenge is solved (by any user) --')); 37 | 38 | form_select($opts, 'Relies on', 'id', $challenge['relies_on'], 'title', 'category'); 39 | 40 | form_input_checkbox('Exposed', true); 41 | form_input_text('Available from', date_time()); 42 | form_input_text('Available until', date_time()); 43 | 44 | message_inline_blue('Create and edit challenge to add files.'); 45 | 46 | form_hidden('action', 'new'); 47 | 48 | form_button_submit('Create challenge'); 49 | form_end(); 50 | 51 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/actions/edit_dynamic_menu_item.php: -------------------------------------------------------------------------------- 1 | $_POST['title'], 18 | 'permalink'=>$_POST['permalink'], 19 | 'url'=>$_POST['url'], 20 | 'visibility'=>$_POST['visibility'], 21 | 'min_user_class'=>$_POST['min_user_class'], 22 | 'priority'=>$_POST['priority'], 23 | 'internal_page'=>$_POST['internal_page'] 24 | ), 25 | array( 26 | 'id'=>$_POST['id'] 27 | ) 28 | ); 29 | 30 | invalidate_cache_group(CONST_CACHE_GROUP_NAME_DYNAMIC_MENU); 31 | 32 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_dynamic_menu_item.php?id='.$_POST['id'].'&generic_success=1'); 33 | } 34 | 35 | else if ($_POST['action'] == 'delete') { 36 | 37 | if (!$_POST['delete_confirmation']) { 38 | message_error('Please confirm delete'); 39 | } 40 | 41 | db_delete( 42 | 'dynamic_menu', 43 | array( 44 | 'id'=>$_POST['id'] 45 | ) 46 | ); 47 | 48 | invalidate_cache_group(CONST_CACHE_GROUP_NAME_DYNAMIC_MENU); 49 | 50 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_dynamic_menu.php?generic_success=1'); 51 | } 52 | } -------------------------------------------------------------------------------- /htdocs/admin/actions/edit_category.php: -------------------------------------------------------------------------------- 1 | $_POST['title'], 18 | 'description'=>$_POST['description'], 19 | 'exposed'=>$_POST['exposed'], 20 | 'available_from'=>strtotime($_POST['available_from']), 21 | 'available_until'=>strtotime($_POST['available_until']) 22 | ), 23 | array( 24 | 'id'=>$_POST['id'] 25 | ) 26 | ); 27 | 28 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_category.php?id='.$_POST['id'].'&generic_success=1'); 29 | } 30 | 31 | else if ($_POST['action'] == 'delete') { 32 | 33 | if (!$_POST['delete_confirmation']) { 34 | message_error('Please confirm delete'); 35 | } 36 | 37 | db_delete( 38 | 'categories', 39 | array( 40 | 'id'=>$_POST['id'] 41 | ) 42 | ); 43 | 44 | $challenges = db_select_all( 45 | 'challenges', 46 | array('id'), 47 | array('category' => $_POST['id']) 48 | ); 49 | 50 | foreach ($challenges as $challenge) { 51 | delete_challenge_cascading($challenge['id']); 52 | } 53 | 54 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . '?generic_success=1'); 55 | } 56 | } -------------------------------------------------------------------------------- /htdocs/admin/actions/edit_hint.php: -------------------------------------------------------------------------------- 1 | $_POST['id'] 19 | ) 20 | ); 21 | 22 | if ($_POST['action'] == 'edit') { 23 | 24 | db_update( 25 | 'hints', 26 | array( 27 | 'body'=>$_POST['body'], 28 | 'challenge'=>$_POST['challenge'], 29 | 'visible'=>$_POST['visible'] 30 | ), 31 | array( 32 | 'id'=>$_POST['id'] 33 | ) 34 | ); 35 | 36 | invalidate_cache(CONST_CACHE_NAME_HINTS); 37 | invalidate_cache(CONST_CACHE_NAME_CHALLENGE_HINTS . $challenge['id']); 38 | 39 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_hint.php?id='.htmlspecialchars($_POST['id']).'&generic_success=1'); 40 | } 41 | 42 | else if ($_POST['action'] == 'delete') { 43 | 44 | if (!$_POST['delete_confirmation']) { 45 | message_error('Please confirm delete'); 46 | } 47 | 48 | db_delete( 49 | 'hints', 50 | array( 51 | 'id'=>$_POST['id'] 52 | ) 53 | ); 54 | 55 | invalidate_cache(CONST_CACHE_NAME_HINTS); 56 | invalidate_cache(CONST_CACHE_NAME_CHALLENGE_HINTS . $challenge['id']); 57 | 58 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_hints.php?generic_success=1'); 59 | } 60 | } -------------------------------------------------------------------------------- /htdocs/country.php: -------------------------------------------------------------------------------- 1 | $_GET['code'] 20 | ) 21 | ); 22 | 23 | if (!$country) { 24 | message_error(lang_get('please_supply_country_code')); 25 | } 26 | 27 | head($country['country_name']); 28 | 29 | if (cache_start(CONST_CACHE_NAME_COUNTRY . $_GET['code'], Config::get('MELLIVORA_CONFIG_CACHE_TIME_COUNTRIES'))) { 30 | 31 | section_head(htmlspecialchars($country['country_name']) . country_flag_link($country['country_name'], $country['country_code'], true), '', false); 32 | 33 | $scores = db_query_fetch_all(' 34 | SELECT 35 | u.id AS user_id, 36 | u.team_name, 37 | u.competing, 38 | co.id AS country_id, 39 | co.country_name, 40 | co.country_code, 41 | SUM(c.points) AS score, 42 | MAX(s.added) AS tiebreaker 43 | FROM users AS u 44 | LEFT JOIN countries AS co ON co.id = u.country_id 45 | LEFT JOIN submissions AS s ON u.id = s.user_id AND s.correct = 1 46 | LEFT JOIN challenges AS c ON c.id = s.challenge 47 | WHERE u.competing = 1 AND co.id = :country_id 48 | GROUP BY u.id 49 | ORDER BY score DESC, tiebreaker ASC', 50 | array( 51 | 'country_id'=>$country['id'] 52 | ) 53 | ); 54 | 55 | scoreboard($scores); 56 | 57 | cache_end(CONST_CACHE_NAME_COUNTRY . $_GET['code']); 58 | } 59 | 60 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/actions/new_challenge.php: -------------------------------------------------------------------------------- 1 | time(), 23 | 'added_by' => $_SESSION['id'], 24 | 'title' => $_POST['title'], 25 | 'description' => $_POST['description'], 26 | 'flag' => $_POST['flag'], 27 | 'automark' => $_POST['automark'], 28 | 'case_insensitive' => $_POST['case_insensitive'], 29 | 'points' => empty_to_zero($_POST['points']), 30 | 'category' => $_POST['category'], 31 | 'num_attempts_allowed' => empty_to_zero($_POST['num_attempts_allowed']), 32 | 'min_seconds_between_submissions' => empty_to_zero($_POST['min_seconds_between_submissions']), 33 | 'relies_on'=>$_POST['relies_on'], 34 | 'exposed' => $_POST['exposed'], 35 | 'available_from' => strtotime($_POST['available_from']), 36 | 'available_until' => strtotime($_POST['available_until']) 37 | ) 38 | ); 39 | 40 | if ($id) { 41 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_challenge.php?id=' . $id); 42 | } else { 43 | message_error('Could not insert new challenge.'); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /htdocs/admin/edit_user.php: -------------------------------------------------------------------------------- 1 | $_GET['id']) 19 | ); 20 | 21 | head('Site management'); 22 | menu_management(); 23 | 24 | section_subhead('Edit user: ' . $user['team_name']); 25 | 26 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_user'); 27 | form_input_text('Email', $user['email']); 28 | form_input_text('Team name', $user['team_name']); 29 | 30 | $opts = db_query_fetch_all('SELECT * FROM countries ORDER BY country_name ASC'); 31 | form_select($opts, 'Country', 'id', $user['country_id'], 'country_name'); 32 | 33 | form_input_checkbox('Enabled', $user['enabled']); 34 | form_input_checkbox('Competing', $user['competing']); 35 | form_hidden('action', 'edit'); 36 | form_hidden('id', $_GET['id']); 37 | form_button_submit('Save changes'); 38 | form_end(); 39 | 40 | section_subhead('Reset password'); 41 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_user'); 42 | form_input_checkbox('Reset confirmation'); 43 | form_hidden('action', 'reset_password'); 44 | form_hidden('id', $_GET['id']); 45 | form_button_submit('Reset password', 'warning'); 46 | form_end(); 47 | 48 | section_subhead('Delete user'); 49 | form_start(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'actions/edit_user'); 50 | form_input_checkbox('Delete confirmation'); 51 | form_hidden('action', 'delete'); 52 | form_hidden('id', $_GET['id']); 53 | message_inline_red('Warning! This will delete all submissions made by this user!'); 54 | form_button_submit('Delete user', 'danger'); 55 | 56 | foot(); -------------------------------------------------------------------------------- /include/constants.inc.php: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | Title 31 | Links to 32 | visibility 33 | Min user class 34 | Manage 35 | 36 | 37 | 38 | '; 39 | 40 | foreach($menu_items as $item) { 41 | echo ' 42 | 43 | ',htmlspecialchars($item['title']),' 44 | ', 45 | ($item['link_title'] ? 46 | ''.htmlspecialchars($item['link_title']).'' : 47 | ''.short_description($item['url'], 20).'' 48 | ),' 49 | 50 | ',visibility_enum_to_name($item['visibility']), ' 51 | ',user_class_name($item['min_user_class']), ' 52 | Edit 53 | 54 | '; 55 | } 56 | 57 | echo ' 58 | 59 | 60 | '; 61 | 62 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/actions/list_submissions.php: -------------------------------------------------------------------------------- 1 | $_POST['id'] 18 | ) 19 | ); 20 | 21 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_submissions.php?generic_success=1'); 22 | } 23 | 24 | else if ($_POST['action'] == 'mark_incorrect') { 25 | 26 | db_update('submissions', array('correct'=>0, 'marked'=>1), array('id'=>$_POST['id'])); 27 | 28 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_submissions.php?generic_success=1'); 29 | } 30 | 31 | else if ($_POST['action'] == 'mark_correct') { 32 | 33 | $submission = db_select_one( 34 | 'submissions', 35 | array( 36 | 'user_id', 37 | 'challenge', 38 | 'correct' 39 | ), 40 | array( 41 | 'id'=>$_POST['id'] 42 | ) 43 | ); 44 | 45 | $num_correct_submissions = db_count_num( 46 | 'submissions', 47 | array( 48 | 'user_id'=>$submission['user_id'], 49 | 'challenge'=>$submission['challenge'], 50 | 'correct'=>1 51 | ) 52 | ); 53 | 54 | if ($num_correct_submissions > 0) { 55 | message_error('This user already has a correct submission for this challenge'); 56 | } 57 | 58 | db_update('submissions', array('correct'=>1, 'marked'=>1), array('id'=>$_POST['id'])); 59 | 60 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_submissions.php?generic_success=1'); 61 | } 62 | } -------------------------------------------------------------------------------- /htdocs/hints.php: -------------------------------------------------------------------------------- 1 | UNIX_TIMESTAMP() AND 24 | h.visible = 1 AND 25 | c.exposed = 1 AND 26 | ca.exposed = 1 27 | ORDER BY h.id DESC 28 | '); 29 | 30 | if (!count($hints)) { 31 | message_generic( 32 | lang_get('hints'), 33 | lang_get('no_hints_available'), 34 | false 35 | ); 36 | } 37 | 38 | section_head('Hints'); 39 | 40 | echo ' 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | '; 52 | 53 | foreach ($hints as $hint) { 54 | echo ' 55 | 56 | 57 | 58 | 59 | 60 | 61 | '; 62 | } 63 | 64 | echo ' 65 | 66 |
',lang_get('category'),'',lang_get('challenge'),'',lang_get('added'),'',lang_get('hint'),'
',htmlspecialchars($hint['category_title']),'',htmlspecialchars($hint['title']),'',time_elapsed($hint['added']),' ago',htmlspecialchars($hint['body']),'
67 | '; 68 | 69 | cache_end(CONST_CACHE_NAME_HINTS); 70 | } 71 | 72 | foot(); -------------------------------------------------------------------------------- /install/docker/README.md: -------------------------------------------------------------------------------- 1 | Mellivora and Docker 2 | ========= 3 | 4 | Mellivora is easy to use with docker-compose. 5 | Mellivora comes with an included docker-compose configuration intended for development use. 6 | If you're looking to run Mellivora using Docker(-compose) in production, a good place to start might be to copy the provided docker-compose config and changing it to suit your needs. 7 | 8 | ### Preliminary 9 | 10 | This assumes you have [Docker](https://docs.docker.com/) and [docker-compose](https://docs.docker.com/compose/) installed. 11 | 12 | ### Run Mellivora 13 | 14 | Run 15 | 16 | ``` 17 | docker-compose -f docker-compose.dev.yml up 18 | ``` 19 | 20 | to start with dev mode settings. 21 | 22 | Run 23 | 24 | ``` 25 | sudo chown -R www-data:www-data writable/ 26 | ``` 27 | 28 | in the Mellivora home directory to give Apache the permissions necessary for challenge file upload and caching. 29 | 30 | #### Create an admin user 31 | 32 | - Visit [http://localhost/](http://localhost/) which should now display the Mellivora landing page. 33 | - Register a new user. You will probably get an error about emails not working. The user is created and functional despite the error. 34 | - Go to [http://localhost:18080](http://localhost:18080) where Adminer should be running (assuming you're running in dev mode). Log in with 35 | ``` 36 | Server: db 37 | Username: root 38 | Password: password 39 | Database: mellivora 40 | ``` 41 | 42 | - To make your user an administrator, go to "SQL command" in the menu and run 43 | 44 | ```sh 45 | UPDATE users SET class = 100 WHERE id = 1; 46 | ``` 47 | 48 | - Log in at [http://localhost/](http://localhost/). Done! 49 | 50 | ### Tips 51 | 52 | - The ``dev`` docker-compose profile mounts to use files directly from the host. Making changes to files on disk will result in changes to the running instance without rebuilding the container. 53 | - If you are making changes to composer requirements, you will need to delete/rebuild the docker image ``composerdependencies``. 54 | - Call ``docker-compose -f docker-compose.dev.yml up --build`` to rebuild and start. 55 | - Copy ``include/config/config.default.inc.php`` to ``include/config/config.inc.php`` to make your configuration changes. -------------------------------------------------------------------------------- /include/layout/login_dialog.inc.php: -------------------------------------------------------------------------------- 1 | 6 | 32 | 33 | '; 34 | } -------------------------------------------------------------------------------- /htdocs/reset_password.php: -------------------------------------------------------------------------------- 1 | $_GET['auth_key'], 17 | 'user_id' => $_GET['id'] 18 | ) 19 | ); 20 | 21 | if (!$auth['user_id']) { 22 | message_error(lang_get('no_reset_data')); 23 | } 24 | } 25 | 26 | // start here 27 | if (!isset($_GET['action'])) { 28 | 29 | head(lang_get('reset_password')); 30 | echo ' 31 |
32 | 33 | 34 | 35 | '; 36 | 37 | if (Config::get('MELLIVORA_CONFIG_RECAPTCHA_ENABLE_PUBLIC')) { 38 | display_captcha(); 39 | } 40 | 41 | echo ' 42 | 43 |
44 | '; 45 | foot(); 46 | } 47 | 48 | // return from password reset email here 49 | else if ($_GET['action']=='choose_password' && is_valid_id($auth['user_id'])) { 50 | 51 | head(lang_get('choose_password')); 52 | echo ' 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 | '; 62 | foot(); 63 | } -------------------------------------------------------------------------------- /htdocs/admin/list_restrict_email.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | Rule 20 | Added 21 | Added by 22 | Type 23 | Priority 24 | Enabled 25 | Manage 26 | 27 | 28 | 29 | '; 30 | 31 | $rules = db_query_fetch_all(' 32 | SELECT 33 | re.id, 34 | re.added, 35 | re.added_by, 36 | re.rule, 37 | re.enabled, 38 | re.white, 39 | re.priority, 40 | u.team_name 41 | FROM restrict_email AS re 42 | LEFT JOIN users AS u ON re.added_by = u.id 43 | ORDER BY re.priority ASC' 44 | ); 45 | 46 | foreach($rules as $rule) { 47 | echo ' 48 | 49 | ',htmlspecialchars($rule['rule']),' 50 | ',date_time($rule['added']),' 51 | ',htmlspecialchars($rule['team_name']),' 52 | 53 | ',($rule['white'] ? 54 | 'Whitelisted' : 55 | 'Blacklisted'),' 56 | 57 | ',number_format($rule['priority']),' 58 | ',($rule['enabled'] ? 'Yes' : 'No'), ' 59 | 60 | Edit 61 | 62 | 63 | '; 64 | } 65 | 66 | echo ' 67 | 68 | 69 | '; 70 | 71 | foot(); -------------------------------------------------------------------------------- /htdocs/admin/list_ip_log.php: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | Team name 27 | Hostname 28 | First used 29 | Last used 30 | Times used 31 | 32 | 33 | 34 | '; 35 | 36 | $query = 'SELECT 37 | INET_NTOA(ipl.ip) AS ip, 38 | ipl.added, 39 | ipl.last_used, 40 | ipl.times_used, 41 | u.team_name, 42 | u.id AS user_id 43 | FROM ip_log AS ipl 44 | LEFT JOIN users AS u ON ipl.user_id = u.id 45 | '; 46 | 47 | if (!empty($where)) { 48 | $query .= 'WHERE '.implode('=? AND ', array_keys($where)).'=? '; 49 | } 50 | 51 | $entries = db_query_fetch_all( 52 | $query, 53 | array_values($where) 54 | ); 55 | 56 | foreach ($entries as $entry) { 57 | echo ' 58 | 59 | 60 | 61 | ', htmlspecialchars($entry['team_name']), ' 62 | 63 | 64 | ', (Config::get('MELLIVORA_CONFIG_GET_IP_HOST_BY_ADDRESS') ? htmlspecialchars(gethostbyaddr($entry['ip'])) : 'Lookup disabled in config'), ' 65 | ', date_time($entry['added']), ' 66 | ', date_time($entry['last_used']), ' 67 | ', number_format($entry['times_used']), ' 68 | 69 | '; 70 | } 71 | 72 | echo ' 73 | 74 | 75 | '; 76 | 77 | foot(); -------------------------------------------------------------------------------- /htdocs/profile.php: -------------------------------------------------------------------------------- 1 | $_SESSION['id']) 18 | ); 19 | 20 | head(lang_get('profile')); 21 | 22 | section_subhead( 23 | lang_get('profile_settings'), 24 | '| '.lang_get('view_public_profile').'', 25 | false 26 | ); 27 | 28 | form_start('actions/profile'); 29 | form_input_text('Email', $user['email'], array('disabled'=>true)); 30 | form_input_text('Team name', $user['team_name'], array('disabled'=>true)); 31 | 32 | $opts = db_query_fetch_all('SELECT * FROM countries ORDER BY country_name ASC'); 33 | form_select($opts, 'Country', 'id', $user['country_id'], 'country_name'); 34 | 35 | form_hidden('action', 'edit'); 36 | form_button_submit(lang_get('save_changes')); 37 | form_end(); 38 | 39 | section_subhead(lang_get('two_factor_auth'), lang_get('using_totp')); 40 | form_start('actions/profile'); 41 | if ($user['2fa_status'] == 'generated') { 42 | form_generic('QR', 'QR'); 43 | form_input_text('Code'); 44 | form_hidden('action', '2fa_enable'); 45 | form_button_submit(lang_get('enable_two_factor_auth')); 46 | } 47 | 48 | else if ($user['2fa_status'] == 'disabled') { 49 | form_hidden('action', '2fa_generate'); 50 | form_button_submit(lang_get('generate_codes')); 51 | } 52 | 53 | else if ($user['2fa_status'] == 'enabled') { 54 | form_generic('QR', 'QR'); 55 | form_hidden('action', '2fa_disable'); 56 | form_button_submit(lang_get('disable_two_factor_auth'), 'danger'); 57 | } 58 | form_end(); 59 | 60 | section_subhead(lang_get('reset_password')); 61 | form_start('actions/profile'); 62 | form_input_password('Current password'); 63 | form_input_password('New password'); 64 | form_input_password('New password again'); 65 | form_hidden('action', 'reset_password'); 66 | form_input_captcha(); 67 | form_button_submit(lang_get('reset_password'), 'warning'); 68 | form_end(); 69 | 70 | foot(); -------------------------------------------------------------------------------- /tests/run_tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | COMPOSE="docker-compose -f docker-compose.test.yml" 5 | 6 | # since Codeception is limited to one db dump to import, 7 | # we need to collect all our SQL dump data into one file 8 | bash "${DIR}/build_sql_dump" 9 | 10 | ${COMPOSE} up -d 11 | echo "Flakily waiting for MySQL docker image to finish starting.." && sleep 10; 12 | ${COMPOSE} run --rm codecept clean && 13 | ${COMPOSE} run --rm codecept run "$@" 14 | 15 | TEST_EXIT_CODE="${?}" 16 | 17 | # output extra information if tests are running on Travis CI 18 | if [ "${TEST_EXIT_CODE}" != "0" ] && [ "${TRAVIS_BUILD_DIR}" != "" ]; 19 | then 20 | cat << EOF 21 | 22 | Codecept exited with a non-zero status. Assuming error. 23 | 24 | EOF 25 | 26 | echo "--- Stored error pages ---" 27 | for file_name in ${DIR}/codeception/_output/*.html; do 28 | echo "--- ${file_name} ---" 29 | cat "${file_name}" 30 | echo 31 | echo "--- End ${file_name} ---" 32 | done 33 | echo "--- End stored error pages ---" 34 | 35 | 36 | echo "--- Uploading screenshots to Imgur ---" 37 | echo "Creating album.." 38 | create_album_resp=$(curl -H "Authorization: Client-ID ${IMGUR_CLIENT_ID}" -X POST "https://api.imgur.com/3/album?title=$(date '+%Y-%m-%d-%H-%M-%S')&privacy=hidden") 39 | album_key=$(echo "${create_album_resp}" | egrep --only-matching '"id":"[a-zA-Z0-9]+"' | egrep --only-matching '[a-zA-Z0-9]{3,}') 40 | album_delete_key=$(echo "${create_album_resp}" | egrep --only-matching '"deletehash":"[a-zA-Z0-9]+"' | egrep --only-matching '[a-zA-Z0-9]{11,}') 41 | 42 | echo 43 | echo "Created album at: https://imgur.com/a/${album_key}" 44 | echo 45 | 46 | for file_name in ${DIR}/codeception/_output/*.png; do 47 | echo "Uploading image: ${file_name}" 48 | curl -H "Authorization: Client-ID ${IMGUR_CLIENT_ID}" -F "image=@${file_name}" "https://api.imgur.com/3/upload?album=${album_delete_key}" > /dev/null 49 | done 50 | 51 | echo "To delete Imgur album, call: curl -H 'Authorization: Client-ID [CLIENT_ID]' -X DELETE https://api.imgur.com/3/album/${album_delete_key}" 52 | 53 | echo 54 | echo "--- Finished uploading screenshots ---" 55 | 56 | cat << EOF 57 | 58 | 59 | Tests failed; See above. 60 | 61 | EOF 62 | fi 63 | 64 | ${COMPOSE} down 65 | 66 | exit ${TEST_EXIT_CODE} 67 | -------------------------------------------------------------------------------- /htdocs/admin/actions/edit_user.php: -------------------------------------------------------------------------------- 1 | $_POST['email'], 20 | 'team_name'=>$_POST['team_name'], 21 | 'enabled'=>$_POST['enabled'], 22 | 'competing'=>$_POST['competing'], 23 | 'country_id'=>$_POST['country'] 24 | ), 25 | array( 26 | 'id'=>$_POST['id'] 27 | ) 28 | ); 29 | 30 | invalidate_cache(CONST_CACHE_NAME_USER . $_POST['id']); 31 | 32 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_users.php?generic_success=1'); 33 | } 34 | 35 | else if ($_POST['action'] == 'delete') { 36 | 37 | if (!$_POST['delete_confirmation']) { 38 | message_error('Please confirm delete'); 39 | } 40 | 41 | db_delete( 42 | 'users', 43 | array( 44 | 'id'=>$_POST['id'] 45 | ) 46 | ); 47 | 48 | db_delete( 49 | 'submissions', 50 | array( 51 | 'user_id'=>$_POST['id'] 52 | ) 53 | ); 54 | 55 | db_delete( 56 | 'ip_log', 57 | array( 58 | 'user_id'=>$_POST['id'] 59 | ) 60 | ); 61 | 62 | db_delete( 63 | 'cookie_tokens', 64 | array( 65 | 'user_id'=>$_POST['id'] 66 | ) 67 | ); 68 | 69 | invalidate_cache(CONST_CACHE_NAME_USER . $_POST['id']); 70 | 71 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'list_users.php?generic_success=1'); 72 | } 73 | 74 | else if ($_POST['action'] == 'reset_password') { 75 | $new_password = generate_random_string(8); 76 | $new_passhash = make_passhash($new_password); 77 | 78 | db_update( 79 | 'users', 80 | array( 81 | 'passhash'=>$new_passhash 82 | ), 83 | array( 84 | 'id'=>$_POST['id'] 85 | ) 86 | ); 87 | 88 | message_generic('Success', 'Users new password is: ' . $new_password); 89 | } 90 | } -------------------------------------------------------------------------------- /htdocs/admin/actions/edit_challenge.php: -------------------------------------------------------------------------------- 1 | $_POST['title'], 18 | 'description'=>$_POST['description'], 19 | 'flag'=>$_POST['flag'], 20 | 'automark'=>$_POST['automark'], 21 | 'case_insensitive'=>$_POST['case_insensitive'], 22 | 'points'=>$_POST['points'], 23 | 'category'=>$_POST['category'], 24 | 'exposed'=>$_POST['exposed'], 25 | 'available_from'=>strtotime($_POST['available_from']), 26 | 'available_until'=>strtotime($_POST['available_until']), 27 | 'num_attempts_allowed'=>$_POST['num_attempts_allowed'], 28 | 'min_seconds_between_submissions'=>$_POST['min_seconds_between_submissions'], 29 | 'relies_on'=>$_POST['relies_on'] 30 | ), 31 | array('id'=>$_POST['id']) 32 | ); 33 | 34 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_challenge.php?id='.$_POST['id'].'&generic_success=1'); 35 | } 36 | 37 | else if ($_POST['action'] == 'delete') { 38 | 39 | if (!$_POST['delete_confirmation']) { 40 | message_error('Please confirm delete'); 41 | } 42 | 43 | delete_challenge_cascading($_POST['id']); 44 | 45 | invalidate_cache(CONST_CACHE_NAME_FILES . $_POST['id']); 46 | invalidate_cache(CONST_CACHE_NAME_CHALLENGE_HINTS . $_POST['id']); 47 | 48 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . '?generic_success=1'); 49 | } 50 | 51 | else if ($_POST['action'] == 'upload_file') { 52 | 53 | store_file($_POST['id'], $_FILES['file']); 54 | 55 | invalidate_cache(CONST_CACHE_NAME_FILES . $_POST['id']); 56 | 57 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_challenge.php?id='.$_POST['id'].'&generic_success=1'); 58 | } 59 | 60 | else if ($_POST['action'] == 'delete_file') { 61 | 62 | delete_file($_POST['id']); 63 | 64 | invalidate_cache(CONST_CACHE_NAME_FILES . $_POST['id']); 65 | 66 | redirect(Config::get('MELLIVORA_CONFIG_SITE_ADMIN_RELPATH') . 'edit_challenge.php?id='.$_POST['challenge_id'].'&generic_success=1'); 67 | } 68 | } -------------------------------------------------------------------------------- /htdocs/admin/list_exceptions.php: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | Message 21 | Added 22 | User 23 | IP 24 | 25 | 26 | 27 | '; 28 | 29 | $where = array(); 30 | if (is_valid_id(array_get($_GET, 'user_id'))) { 31 | $where['added_by'] = $_GET['user_id']; 32 | } 33 | 34 | $from = get_pager_from($_GET); 35 | $num_exceptions = db_count_num('exceptions', $where); 36 | 37 | pager( 38 | Config::get('MELLIVORA_CONFIG_SITE_ADMIN_URL').'list_exceptions', 39 | $num_exceptions, 40 | CONST_NUM_EXCEPTIONS_PER_PAGE, 41 | $from 42 | ); 43 | 44 | $query = 'SELECT 45 | e.id, 46 | e.message, 47 | e.added, 48 | e.added_by, 49 | e.trace, 50 | INET_NTOA(e.user_ip) AS user_ip, 51 | u.team_name 52 | FROM exceptions AS e 53 | LEFT JOIN users AS u ON u.id = e.added_by 54 | '; 55 | 56 | if (!empty($where)) { 57 | $query .= 'WHERE '.implode('=? AND ', array_keys($where)).'=? '; 58 | } 59 | 60 | $query .= 'ORDER BY e.id DESC 61 | LIMIT '.$from.', '.CONST_NUM_EXCEPTIONS_PER_PAGE; 62 | 63 | $exceptions = db_query_fetch_all($query, array_values($where)); 64 | 65 | foreach($exceptions as $exception) { 66 | echo ' 67 | 68 | ',htmlspecialchars($exception['message']),' 69 | ',date_time($exception['added']),' 70 | ',($exception['added_by'] ? 71 | ''.htmlspecialchars($exception['team_name']).'' 72 | : 73 | 'N/A'),' 74 | 75 | ',htmlspecialchars($exception['user_ip']),' 76 | 77 | 78 | 79 |
',nl2br(htmlspecialchars($exception['trace'])),' 
80 | 81 | 82 | '; 83 | } 84 | 85 | echo ' 86 | 87 | 88 | '; 89 | 90 | foot(); 91 | -------------------------------------------------------------------------------- /include/layout/messages.inc.php: -------------------------------------------------------------------------------- 1 | ',htmlspecialchars($message),'

'; 49 | } 50 | 51 | function message_inline_blue ($message, $strip_html = true) { 52 | echo '
',($strip_html ? htmlspecialchars($message) : $message),'
'; 53 | } 54 | 55 | function message_inline_red ($message, $strip_html = true) { 56 | echo '
',($strip_html ? htmlspecialchars($message) : $message),'
'; 57 | } 58 | 59 | function message_inline_yellow ($message, $strip_html = true) { 60 | echo '
',($strip_html ? htmlspecialchars($message) : $message),'
'; 61 | } 62 | 63 | function message_inline_green ($message, $strip_html = true) { 64 | echo '
',($strip_html ? htmlspecialchars($message) : $message),'
'; 65 | } 66 | 67 | function message_dialog ($message, $title, $closeText, $class) { 68 | 69 | echo ' 70 | 83 | '; 84 | } 85 | 86 | function message_correct_flag () { 87 | echo '
'; 88 | } -------------------------------------------------------------------------------- /tests/codeception/acceptance/admin/ManageNewsCest.php: -------------------------------------------------------------------------------- 1 | logInAsAnAdmin(); 7 | 8 | $I->amOnAdminHome(); 9 | $I->click('News'); 10 | $I->click('Add news item'); 11 | 12 | $I->waitForText('New news item'); 13 | $I->seeInCurrentUrl('/new_news'); 14 | 15 | $title = time().'title'; 16 | $body = time().'body'; 17 | 18 | $I->fillField('title', $title); 19 | $I->fillField('body', $body); 20 | $I->click('Publish news item'); 21 | 22 | $I->seeInCurrentUrl('/edit_news'); 23 | $I->seeInField('title', $title); 24 | $I->seeInField('body', $body); 25 | 26 | $I->amOnPage('/home'); 27 | $I->see($title); 28 | $I->see($body); 29 | } 30 | 31 | /** 32 | * @depends shouldBeAbleToCreateANewNewsPost 33 | */ 34 | public function shouldBeAbleToEditANewsPost(AcceptanceTester $I) { 35 | $I->logInAsAnAdmin(); 36 | 37 | $I->amOnListNews(); 38 | $I->click('Edit'); 39 | 40 | $I->waitForText('Edit news item'); 41 | $I->seeInCurrentUrl('/edit_news'); 42 | 43 | $title = time().'title'; 44 | $body = time().'body'; 45 | 46 | $I->fillField('title', $title); 47 | $I->fillField('body', $body); 48 | $I->click('Save changes'); 49 | 50 | $I->seeInCurrentUrl('/edit_news'); 51 | $I->seeInField('title', $title); 52 | $I->seeInField('body', $body); 53 | 54 | $I->amOnPage('/home'); 55 | $I->see($title); 56 | $I->see($body); 57 | } 58 | 59 | /** 60 | * @depends shouldBeAbleToEditANewsPost 61 | */ 62 | public function shouldNotBeAbleToDeleteANewsPostWithoutTickingTheConfirmationBox(AcceptanceTester $I) { 63 | $I->logInAsAnAdmin(); 64 | 65 | $I->amOnListNews(); 66 | $I->click('Edit'); 67 | 68 | $I->waitForText('Edit news item'); 69 | $I->seeInCurrentUrl('/edit_news'); 70 | $I->click('Delete news item'); 71 | 72 | $I->see('Error'); 73 | $I->see('Please confirm delete'); 74 | 75 | $I->amOnListNews(); 76 | $I->see('Edit'); 77 | } 78 | 79 | /** 80 | * @depends shouldNotBeAbleToDeleteANewsPostWithoutTickingTheConfirmationBox 81 | */ 82 | public function shouldBeAbleToDeleteANewsPostWhenTickingTheConfirmationBox(AcceptanceTester $I) { 83 | $I->logInAsAnAdmin(); 84 | 85 | $I->amOnListNews(); 86 | $I->click('Edit'); 87 | 88 | $I->waitForText('Edit news item'); 89 | $I->seeInCurrentUrl('/edit_news'); 90 | $I->checkOption('#delete_confirmation'); 91 | $I->click('Delete news item'); 92 | 93 | $I->seeInCurrentUrl('/list_news'); 94 | $I->dontSee('Edit'); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /htdocs/register.php: -------------------------------------------------------------------------------- 1 | ',lang_get('register_your_team'),' 17 |

18 | ',lang_get( 19 | 'account_signup_information', 20 | array( 21 | 'password_information' => (Config::get('MELLIVORA_CONFIG_ACCOUNTS_EMAIL_PASSWORD_ON_SIGNUP') ? lang_get('email_password_on_signup') : '') 22 | ) 23 | ),' 24 |

25 |
26 | 27 | 28 | ',(!Config::get('MELLIVORA_CONFIG_ACCOUNTS_EMAIL_PASSWORD_ON_SIGNUP') ? '' : ''); 29 | 30 | if (cache_start(CONST_CACHE_NAME_REGISTER, Config::get('MELLIVORA_CONFIG_CACHE_TIME_REGISTER'))) { 31 | $user_types = db_select_all( 32 | 'user_types', 33 | array( 34 | 'id', 35 | 'title', 36 | 'description' 37 | ) 38 | ); 39 | 40 | if (!empty($user_types)) { 41 | echo ''; 49 | } 50 | 51 | country_select(); 52 | cache_end(CONST_CACHE_NAME_REGISTER); 53 | } 54 | 55 | if (Config::get('MELLIVORA_CONFIG_RECAPTCHA_ENABLE_PUBLIC')) { 56 | display_captcha(); 57 | } 58 | 59 | echo ' 60 | 61 | 62 |
63 | '; 64 | 65 | } else { 66 | message_inline_blue( 67 | 'Registration is currently closed, but you can still register your interest for upcoming events.', 68 | false 69 | ); 70 | } 71 | 72 | foot(); 73 | -------------------------------------------------------------------------------- /install/caddy/README.md: -------------------------------------------------------------------------------- 1 | Mellivora on Caddy Server 2 | ========= 3 | 4 | This readme serves as a super-quick guide to setting up Mellivora on a Caddy Server. 5 | 6 | Estimated setup time: 15 minutes. 7 | 8 | ### Preliminary 9 | 10 | Connect to your server 11 | 12 | ### Installation 13 | 14 | Install required PHP extensions 15 | ```sh 16 | sudo apt-get install php-curl php-pear php-mbstring 17 | ``` 18 | 19 | Install Composer 20 | ```sh 21 | curl -sS https://getcomposer.org/installer | php 22 | sudo mv composer.phar /usr/local/bin/composer 23 | ``` 24 | 25 | Make /var/www/ writable. 26 | ```sh 27 | sudo chown -R $(whoami):$(whoami) /var/www/ 28 | cd /var/www/ 29 | ``` 30 | 31 | Install git and clone the repo. 32 | ```sh 33 | sudo apt-get install -y git 34 | git clone https://github.com/Nakiami/mellivora.git 35 | ``` 36 | 37 | Install unzip package 38 | ```sh 39 | sudo apt-get install unzip 40 | ``` 41 | 42 | Fetch required dependencies using Composer 43 | ```sh 44 | cd /var/www/mellivora/ 45 | composer install 46 | ``` 47 | 48 | Copy and edit configuration file. 49 | ```sh 50 | cp /var/www/mellivora/include/config/config.default.inc.php /var/www/mellivora/include/config/config.inc.php 51 | cp /var/www/mellivora/include/config/db.default.inc.php /var/www/mellivora/include/config/db.inc.php 52 | vim /var/www/mellivora/include/config/config.inc.php 53 | ``` 54 | 55 | Make the writable directory writable. 56 | ```sh 57 | sudo chown -R www-data:www-data /var/www/mellivora/writable/ 58 | ``` 59 | 60 | Copy and edit the CaddyFile config file. 61 | ```sh 62 | sudo cp /var/www/mellivora/install/caddy/Caddyfile /etc/caddy/Caddyfile 63 | sudo vim /etc/caddy/Caddyfile 64 | ``` 65 | 66 | Install mariadb 67 | ```sh 68 | sudo apt-get install mariadb-server 69 | ``` 70 | 71 | Create the Mellivora database and import the provided structure. 72 | ```sh 73 | echo "CREATE DATABASE mellivora CHARACTER SET utf8 COLLATE utf8_general_ci;" | mysql -u root -p 74 | mysql mellivora -u root -p < /var/www/mellivora/install/sql/001-mellivora.sql 75 | mysql mellivora -u root -p < /var/www/mellivora/install/sql/002-countries.sql 76 | ``` 77 | 78 | Create a new MySQL user. 79 | ```sh 80 | echo "GRANT ALL PRIVILEGES ON mellivora.* TO 'YourUserName'@'%' IDENTIFIED BY 'YourPassword';" | mysql -u root -p 81 | ``` 82 | 83 | Update the database config settings to use the database and user we created above. 84 | ```sh 85 | vim /var/www/mellivora/include/config/db.inc.php 86 | ``` 87 | 88 | - Visit https://ctf.[yoursite]/ which should now display the Mellivora landing page. 89 | - Register a new user. 90 | - If you get an error about emails not working, you should set up a local mailserver or set up SMTP in the config. The user is created and functional despite the error. 91 | 92 | Make the user a site moderator. 93 | ```sh 94 | echo "UPDATE users SET class = 100 WHERE id = 1;" | mysql mellivora -u root -p 95 | ``` 96 | 97 | Log in. Done! 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mellivora 2 | ========= 3 | 4 | Mellivora is a CTF engine written in PHP. Want a quick overview? Check out a [screenshot gallery on imgur](https://imgur.com/user/mellivora/posts). Want a quick start? Use [Mellivora with Docker](install/docker/README.md). 5 | 6 |

7 | Mellivora logo 8 |

9 | 10 | ### Features 11 | - Arbitrary categories and challenges. 12 | - Scoreboard with optional multiple team types. 13 | - Manual or automatic free-text submission marking. 14 | - Challenge hints. 15 | - Team progress page. 16 | - Challenge overview page. 17 | - Limit category and challenge exposure to certain times. 18 | - Challenge reveal on parent challenge solve (by any team). 19 | - Optional signup restrictions based on email regex. 20 | - Local or [Amazon S3](https://aws.amazon.com/s3/) challenge file upload. 21 | - Optional automatic MD5 append to files. 22 | - Admin management console with competition overview. 23 | - Create/edit front page news. 24 | - Arbitrary menu items and internal pages. 25 | - Optional total number and time-based submission throttling. 26 | - User management with IP correlation. 27 | - Internal log for catching exceptions. 28 | - [reCAPTCHA](https://www.google.com/recaptcha/) support. 29 | - User-defined or auto-generated passwords on signup. 30 | - User/Email/IP search. 31 | - Configurable caching. 32 | - Caching proxy (like [Cloudflare](https://www.cloudflare.com/)) aware (optional x-forwarded-for trust). 33 | - Optional separate domain for static files. 34 | - [Segment](https://segment.com/) analytics support. 35 | - SMTP email support. Bulk or single email composition. 36 | - TOTP two factor auth support. 37 | - [CTF Time](https://ctftime.org/) compatible JSON scoreboard. 38 | - Self-serve and admin password reset. 39 | - and more ... 40 | 41 | ### Scaling 42 | Mellivora scales well on Amazon Elastic Beanstalk and has support for S3 file storage. 43 | 44 | ### Performance 45 | Mellivora is lightweight. And fast. Very fast. Want to run a large competition on an EC2 micro instance? No problem!? See [benchmarks.md](benchmarks.md) for some possibly unhelpful benchmarks. 46 | 47 | ### Installation 48 | * You can find detailed setup instructions in [install/README.md](install/README.md). 49 | * Run Mellivora easily with docker-compose. See [install/docker/README.md](install/docker/README.md). 50 | 51 | ### Development 52 | [![Build Status](https://app.travis-ci.com/Nakiami/mellivora.svg?branch=master)](https://app.travis-ci.com/Nakiami/mellivora) 53 | 54 | PRs gladly accepted. Test using [Codeception](http://codeception.com/). Read [more about testing here](tests/README.md). 55 | 56 | ### License 57 | This software is licenced under the [GNU General Public License v3 (GPL-3)](http://www.tldrlegal.com/license/gnu-general-public-license-v3-%28gpl-3%29). The "include/thirdparty/" directory contains third party code. Please read their LICENSE files for information on the software availability and distribution. 58 | -------------------------------------------------------------------------------- /include/cache.inc.php: -------------------------------------------------------------------------------- 1 | get($identifier, $group); 15 | } 16 | 17 | function cache_array_save($data, $identifier, $group = 'default') { 18 | global $caches; 19 | 20 | $caches[$group][$identifier]->save($data, $identifier, $group); 21 | } 22 | 23 | function cache_start ($identifier, $lifetime, $group = 'default') { 24 | global $caches; 25 | 26 | // if lifetime is zero, we don't perform caching. 27 | // by returning true, we signal that content needs to be recreated 28 | if (!$lifetime) { 29 | return true; 30 | } 31 | 32 | initialize_cache($identifier, $group, $lifetime, false); 33 | 34 | // return true if cache has expired, and we need to recreate content 35 | // return false if cache is still valid 36 | return !($caches[$group][$identifier]->start($identifier, $group)); 37 | } 38 | 39 | function cache_end ($identifier, $group = 'default') { 40 | global $caches; 41 | 42 | if (!empty($caches[$group][$identifier])) { 43 | $caches[$group][$identifier]->end(); 44 | } 45 | } 46 | 47 | function initialize_cache($identifier, $group, $lifetime, $serialize) { 48 | global $caches; 49 | 50 | // if no caching object exists for this identifier, create it 51 | if (empty($caches[$group][$identifier])) { 52 | $caches[$group][$identifier] = new Cache_Lite_Output( 53 | array( 54 | 'cacheDir' => CONST_PATH_CACHE, 55 | 'lifeTime' => $lifetime, 56 | 'fileNameProtection' => false, 57 | 'automaticSerialization' => $serialize 58 | ) 59 | ); 60 | } 61 | } 62 | 63 | function send_cache_headers ($identifier, $lifetime, $group = 'default') { 64 | header('Cache-Control: '.(user_is_logged_in() ? 'private' : 'public').', max-age=' . $lifetime); 65 | 66 | $path = CONST_PATH_CACHE . 'cache_' . $group . '_' . $identifier; 67 | if (file_exists($path)) { 68 | $time_modified = filemtime($path); 69 | 70 | header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $time_modified) . 'GMT'); 71 | header('Expires: ' . gmdate('D, d M Y H:i:s ', $time_modified + $lifetime) . 'GMT'); 72 | } 73 | } 74 | 75 | function invalidate_cache ($id, $group = 'default') { 76 | $path = CONST_PATH_CACHE . 'cache_' . $group . '_' . $id; 77 | if (file_exists($path)) { 78 | unlink($path); 79 | } 80 | } 81 | 82 | function invalidate_cache_group ($group = 'default') { 83 | $prefix = 'cache_' . $group . '_'; 84 | 85 | $cache_files = scandir(CONST_PATH_CACHE); 86 | foreach ($cache_files as $file) { 87 | if (starts_with($file, $prefix)) { 88 | unlink(CONST_PATH_CACHE . $file); 89 | } 90 | } 91 | } --------------------------------------------------------------------------------