├── 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 |
15 |
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 | Title
16 | Description
17 |
18 |
28 |
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 | ',htmlspecialchars($type['title']),'
29 | ',short_description($type['description'], 50),'
30 | Edit
31 |
15 |
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 | Challenge
16 | Added
17 | Hint
18 | Manage
19 |
37 |
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 | ',htmlspecialchars($hint['title']),'
38 | ',date_time($hint['added']),'
39 | ',htmlspecialchars($hint['body']), '
40 | Edit
41 |
28 |
33 |
34 |
35 | ';
36 |
37 | foreach($pages as $item) {
38 | echo '
39 | Title
29 | visibility
30 | Min user class
31 | Manage
32 |
40 |
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 | ',htmlspecialchars($item['title']),'
41 | ',visibility_enum_to_name($item['visibility']), '
42 | ',user_class_name($item['min_user_class']), '
43 | Edit
44 |
30 |
36 |
37 |
38 | ';
39 |
40 | foreach($menu_items as $item) {
41 | echo '
42 | Title
31 | Links to
32 | visibility
33 | Min user class
34 | Manage
35 |
43 |
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 | ',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 |
42 |
43 |
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 |
44 |
49 |
50 |
51 | ';
52 |
53 | foreach ($hints as $hint) {
54 | echo '
55 | ',lang_get('category'),'
45 | ',lang_get('challenge'),'
46 | ',lang_get('added'),'
47 | ',lang_get('hint'),'
48 |
56 |
61 | ';
62 | }
63 |
64 | echo '
65 |
66 | ',htmlspecialchars($hint['category_title']),'
57 | ',htmlspecialchars($hint['title']),'
58 | ',time_elapsed($hint['added']),' ago
59 | ',htmlspecialchars($hint['body']),'
60 |
19 |
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 | Rule
20 | Added
21 | Added by
22 | Type
23 | Priority
24 | Enabled
25 | Manage
26 |
49 |
63 | ';
64 | }
65 |
66 | echo '
67 |
68 |
69 | ';
70 |
71 | foot();
--------------------------------------------------------------------------------
/htdocs/admin/list_ip_log.php:
--------------------------------------------------------------------------------
1 |
24 |
25 | ',htmlspecialchars($rule['rule']),'
50 | ',date_time($rule['added']),'
51 | ',htmlspecialchars($rule['team_name']),'
52 |
53 | ',($rule['white'] ?
54 | '
57 |
' :
55 | '
'),'
56 | ',number_format($rule['priority']),'
58 | ',($rule['enabled'] ? 'Yes' : 'No'), '
59 |
60 | Edit
61 |
62 |
26 |
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 | Team name
27 | Hostname
28 | First used
29 | Last used
30 | Times used
31 |
59 |
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', '
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 | ');
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', '
');
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 |
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 | Message
21 | Added
22 | User
23 | IP
24 |
68 |
77 | ',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 |
78 |
82 | ';
83 | }
84 |
85 | echo '
86 |
87 |
88 | ';
89 |
90 | foot();
91 |
--------------------------------------------------------------------------------
/include/layout/messages.inc.php:
--------------------------------------------------------------------------------
1 | ',htmlspecialchars($message),'
79 |
81 | ',nl2br(htmlspecialchars($exception['trace'])),'
80 |
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 | 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 |
8 |