From 074c9cbad9da233da9dd96e2dd0a64618a4a07a6 Mon Sep 17 00:00:00 2001 From: Ainsley Ellis Date: Thu, 7 Nov 2024 07:48:32 -0500 Subject: [PATCH] In medias res --- .gitignore | 4 + config/apache/app.sixfold.org.conf | 76 +++ config/php/fpm/app.sixfold.org.ini | 26 + lib/config.php | 129 ++++ lib/partials/assignments.php | 86 +++ lib/partials/feedback.php | 31 + lib/partials/footer.php | 16 + lib/partials/head.php | 27 + lib/partials/header.php | 24 + lib/partials/login-form.php | 17 + lib/partials/submission-info.php | 33 ++ www/account/edit.php | 225 +++++++ www/account/index.php | 23 + www/account/logout.php | 8 + www/assets/css/app.css | 95 +++ www/assets/css/core.css | 907 +++++++++++++++++++++++++++++ www/assets/css/dark-mode.css | 12 + www/assets/js/paypal.js | 130 +++++ www/assets/lockup.svg | 1 + www/assets/sixfold.svg | 8 + www/docs/index.php | 69 +++ www/docs/random.php | 12 + www/errors/404.php | 16 + www/errors/503.php | 18 + www/forgot-password.php | 182 ++++++ www/games/game.php | 208 +++++++ www/games/index.php | 118 ++++ www/games/process-order.php | 208 +++++++ www/games/results.php | 140 +++++ www/games/submit.php | 301 ++++++++++ www/games/update.php | 370 ++++++++++++ www/index.php | 68 +++ www/login.php | 58 ++ www/members/index.php | 105 ++++ www/members/member.php | 69 +++ www/signup.php | 125 ++++ 36 files changed, 3945 insertions(+) create mode 100644 .gitignore create mode 100644 config/apache/app.sixfold.org.conf create mode 100644 config/php/fpm/app.sixfold.org.ini create mode 100644 lib/config.php create mode 100644 lib/partials/assignments.php create mode 100644 lib/partials/feedback.php create mode 100644 lib/partials/footer.php create mode 100644 lib/partials/head.php create mode 100644 lib/partials/header.php create mode 100644 lib/partials/login-form.php create mode 100644 lib/partials/submission-info.php create mode 100644 www/account/edit.php create mode 100644 www/account/index.php create mode 100644 www/account/logout.php create mode 100644 www/assets/css/app.css create mode 100644 www/assets/css/core.css create mode 100644 www/assets/css/dark-mode.css create mode 100644 www/assets/js/paypal.js create mode 100644 www/assets/lockup.svg create mode 100644 www/assets/sixfold.svg create mode 100644 www/docs/index.php create mode 100644 www/docs/random.php create mode 100644 www/errors/404.php create mode 100644 www/errors/503.php create mode 100644 www/forgot-password.php create mode 100644 www/games/game.php create mode 100644 www/games/index.php create mode 100644 www/games/process-order.php create mode 100644 www/games/results.php create mode 100644 www/games/submit.php create mode 100644 www/games/update.php create mode 100644 www/index.php create mode 100644 www/login.php create mode 100644 www/members/index.php create mode 100644 www/members/member.php create mode 100644 www/signup.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c674df6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +config/php/fpm/app.sixfold.org-secrets.ini +db +www/assets/avatars +www/assets/docs diff --git a/config/apache/app.sixfold.org.conf b/config/apache/app.sixfold.org.conf new file mode 100644 index 0000000..b09fe7c --- /dev/null +++ b/config/apache/app.sixfold.org.conf @@ -0,0 +1,76 @@ + + ServerName app.sixfold.org + RedirectPermanent / https://app.sixfold.org/ + + + ServerName app.sixfold.org + + ServerAdmin sixfold@sixfold.org + DocumentRoot /var/www/html/app.sixfold.org/www + + ErrorLog ${APACHE_LOG_DIR}/app.sixfold.org-error.log + CustomLog ${APACHE_LOG_DIR}/app.sixfold.org-access.log combined + + ErrorDocument 404 /errors/404 + ErrorDocument 503 /errors/503 + + RewriteEngine On + + + RewriteCond %{ENV:REDIRECT_STATUS} ="" + RewriteRule .* - [R=404,L] + + + # Received: /filename.php and /filename.php exists in filesystem; Result: 301 redirect to /filename and restart request + RewriteCond %{REQUEST_FILENAME} \.php$ + RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} -f + RewriteRule ^/(.+)\.php$ /$1 [R=301,L] + + # Received: /filename and /filename.php exists in filesystem; Result: change /filename to /filename.php and continue processing + RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} !-f + RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} !-d + RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI}.php -f + RewriteRule ^(.+)$ $1.php [QSA] + + # Received: /filename and /filename.xml exists in filesystem; Result: rewrite to /filename.xml + RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME}.xml -f + RewriteRule (.*) $1.xml + + # Received: /filename and /filename.xhtml exists in filesystem; Result: rewrite to /filename.xhtml + RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME}.xhtml -f + RewriteRule (.*) $1.xhtml + + # Redirect index pages + RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -d + RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME}/index.php -f + RewriteRule (.*) $1/index.php + + # Remove trailing slashes + RewriteRule ^/(.+?)/$ /$1 [R=301,L] + + RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} -d + RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME}/index.xml -f + RewriteRule (.*) $1/index.xml + + RewriteCond %{QUERY_STRING} hash=([a-z0-9]+) [NC] + RewriteRule ^/docs /docs/%1? [L,R=301] + + # Sixfold rewrites + RewriteRule ^/games/([0-9]+)$ /games/game.php?game=$1 + RewriteRule ^/games/([0-9]+)/feedback$ /games/feedback.php?game=$1 + RewriteRule ^/games/([0-9]+)/results$ /games/results.php?game=$1 + RewriteRule ^/games/([0-9]+)/submit$ /games/submit.php?game=$1 + RewriteRule ^/games/([0-9]+)/update$ /games/update.php?game=$1 + + RewriteRule ^/doc/([a-z0-9]+)$ /docs/$1 [L,R=301] + RewriteRule ^/docs/([a-z0-9]+)$ /docs/index.php?hash=$1 [L,QSA] + RewriteRule ^/members/([^\.]+?)$ /members/member.php?handle=$1 [L] + + # PayPal order processing rewrites + RewriteRule /api/orders$ /games/process-order.php + RewriteRule /api/orders/(.*)/capture$ /games/process-order.php + + SSLCertificateFile /etc/letsencrypt/live/app.sixfold.org/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/app.sixfold.org/privkey.pem + Include /etc/letsencrypt/options-ssl-apache.conf + diff --git a/config/php/fpm/app.sixfold.org.ini b/config/php/fpm/app.sixfold.org.ini new file mode 100644 index 0000000..3cebfc7 --- /dev/null +++ b/config/php/fpm/app.sixfold.org.ini @@ -0,0 +1,26 @@ +error_log = "/var/log/local/php-error.log" +display_errors = Off +display_startup_errors = Off +html_errors = Off +log_errors = On +short_open_tag = On +error_reporting = E_ALL +max_execution_time = 120 +allow_url_fopen = false +allow_url_include = false +expose_php = Off + +post_max_size = 10M +upload_max_filesize = 6M + +include_path = /var/www/html/app.sixfold.org/lib +auto_prepend_file = config.php + +[session] +session.use_strict_mode = Off + +[Date] +date.timezone = Etc/UTC + +[app] +app.site_status = "live" diff --git a/lib/config.php b/lib/config.php new file mode 100644 index 0000000..b674a3c --- /dev/null +++ b/lib/config.php @@ -0,0 +1,129 @@ + 0, + "path" => "/", + "domain" => $_SERVER["HTTP_HOST"], + "secure" => false, + "httponly" => true, + "samesite" => "Strict", // Helps mitigate CSRF attacks +]); + +session_start(); + +define("ABS_PATH", $_SERVER["DOCUMENT_ROOT"]); +define("DIRECTORY_DOCS", $_SERVER["DOCUMENT_ROOT"] . "/assets/docs"); +define("UPLOAD_MAX_FILESIZE", 1024 * 1000 * 6); + +define("TEST_COOKIE_NAME", get_cfg_var("secrets.test_cookie_name")); +define("TEST_COOKIE_VALUE", get_cfg_var("secrets.test_cookie_value")); +define("PAYPAL_CLIENT_ID", get_cfg_var("secrets.paypal.client_id")); +define("PAYPAL_CLIENT_SECRET", get_cfg_var("secrets.paypal.client_secret")); +define("PAYPAL_BASE_URL", get_cfg_var("secrets.paypal.base_url")); + +define("CURRENT_URL", parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH)); +define("LOGGED_IN", isset($_SESSION["account"])); +define("IS_ADMIN", LOGGED_IN && $_SESSION["account"]->account_type === 9); + +define( + "COOKIES_ENABLED", + isset($_COOKIE[TEST_COOKIE_NAME]) && + $_COOKIE[TEST_COOKIE_NAME] == TEST_COOKIE_VALUE + ? 1 + : 0 +); + +setcookie(TEST_COOKIE_NAME, TEST_COOKIE_VALUE, [ + "expires" => 0, + "path" => "/", + "domain" => $_SERVER["HTTP_HOST"], + "secure" => false, + "httponly" => true, + "samesite" => "Strict", +]); + +$db = [ + "data" => new PDO(get_cfg_var("secrets.db_url")), +]; + +$stmt = $db["data"]->query("SELECT name, id FROM game_status"); + +foreach ($stmt->fetchAll(PDO::FETCH_KEY_PAIR) as $name => $id) { + define($name, $id); +} +unset($name, $id); + +$time_zone = new DateTimeZone("America/New_York"); +$one_second = new DateInterval("PT1S"); + +function get_status_message($status_code) +{ + if ($status_code === STATUS_ENROLLING) { + return "Enrolling"; + } elseif ($status_code === STATUS_ROUND_ONE) { + return "Round One"; + } elseif ($status_code === STATUS_ROUND_TWO) { + return "Round Two"; + } elseif ($status_code === STATUS_ROUND_THREE) { + return "Round Three"; + } elseif ($status_code === STATUS_REVIEW) { + return "Reviewing Submissions"; + } elseif ($status_code === STATUS_DELAYED) { + return "Delayed"; + } elseif ($status_code === STATUS_DONE) { + return "Completed"; + } else { + return "Unknown Status"; + } +} + +function slugify($string) +{ + $rules = << ; + [-] } [:^L:] > ; + [-[:Separator:]]+ > '-'; +RULES; + + return \Transliterator::createFromRules($rules)->transliterate($string); +} + +/** + * Verify if the submitted password is correct + */ +function password_check($account) +{ + global $db; + + if (substr($account->password, 0, 9) === '$argon2id') { ?> + password + );} else {if (sha1($_POST["password"]) === $account->password): + $stmt = $db["data"]->prepare('UPDATE members + SET password = :password + WHERE email = :email'); + $new_password = password_hash( + $_POST["password"], + PASSWORD_ARGON2ID + ); + + $did_update = $stmt->execute([ + "email" => $account->email, + "password" => $new_password, + ]); + if (!$did_update) { + http_response_code(500); + } + return $did_update; + else: + http_response_code(401); + endif; + + return false;} +} diff --git a/lib/partials/assignments.php b/lib/partials/assignments.php new file mode 100644 index 0000000..1555012 --- /dev/null +++ b/lib/partials/assignments.php @@ -0,0 +1,86 @@ +params = [ + "game_id" => $game->id, + "member_id" => 30436 ?? $_SESSION["account"]->id, + "round" => 1, +]; + +$stmt_assignments = $db["data"]->prepare( + "SELECT submissions.id, title, hash, score FROM assignments + JOIN submissions ON submissions.id = assignments.submission_id + WHERE assignments.game_id = :game_id AND assignments.member_id = :member_id + AND round_number = :round" +); +$stmt_assignments->execute($assignments->params); + +$assignments->round_1 = $stmt_assignments->fetchAll(PDO::FETCH_OBJ); + +$assignments->params["round"] = 2; +$stmt_assignments->execute($assignments->params); +$assignments->round_2 = $stmt_assignments->fetchAll(PDO::FETCH_OBJ); + +$assignments->params["round"] = 3; +$stmt_assignments->execute($assignments->params); +$assignments->round_3 = $stmt_assignments->fetchAll(PDO::FETCH_OBJ); +?> +
+

Your Assignments

+
+ + + + + + + + + + round_1 as $item): ?> + + + + + + +
Round One Assignments for name ?>
TitleScore
title ?>score ?? 'N/A' ?>
+
+
+ + + + + + + + + + round_2 as $item): ?> + + + + + + +
Round Two Assignments for name ?>
TitleScore
title ?>score ?? 'N/A' ?>
+
+
+ + + + + + + + + + round_3 as $item): ?> + + + + + + +
Round Three Assignments for name ?>
TitleScore
title ?>score ?? 'N/A' ?>
+
+
diff --git a/lib/partials/feedback.php b/lib/partials/feedback.php new file mode 100644 index 0000000..ca73f35 --- /dev/null +++ b/lib/partials/feedback.php @@ -0,0 +1,31 @@ +prepare( + "SELECT round_number, score, comment FROM assignments WHERE completed IS NOT NULL AND submission_id = :submission_id" +); +$stmt_fdbck->execute([ + "submission_id" => $submission->id, +]); +$feedback = $stmt_fdbck->fetchAll(PDO::FETCH_OBJ); +?> +
+

Feedback

+
+ + + + + + + + + + + + + + + + +
Feedback for "title ?>"
ScoreComment
score ?>comment) ? mb_convert_encoding($item->comment, 'Windows-1252','utf-8') : 'No comment was provided.' ?>
+
+
diff --git a/lib/partials/footer.php b/lib/partials/footer.php new file mode 100644 index 0000000..4dbaef8 --- /dev/null +++ b/lib/partials/footer.php @@ -0,0 +1,16 @@ + diff --git a/lib/partials/head.php b/lib/partials/head.php new file mode 100644 index 0000000..e375ee2 --- /dev/null +++ b/lib/partials/head.php @@ -0,0 +1,27 @@ + + + + + + <?= $title ? "$title | " . "Sixfold" : "Sixfold" ?> + + + + + + + + + + + + + + + "/> + "/> + + + + + diff --git a/lib/partials/header.php b/lib/partials/header.php new file mode 100644 index 0000000..9d7c739 --- /dev/null +++ b/lib/partials/header.php @@ -0,0 +1,24 @@ +
+ +
+
+ + +
diff --git a/lib/partials/login-form.php b/lib/partials/login-form.php new file mode 100644 index 0000000..e8635a5 --- /dev/null +++ b/lib/partials/login-form.php @@ -0,0 +1,17 @@ +
+ + + + +
+

Forgot your password? Send an email to sixfold [at] sixfold.org.

+
+

Not a member yet?

+

Signing up only takes a second and it's completely free. Join the completely writer-voted journal.

+

Create an account

diff --git a/lib/partials/submission-info.php b/lib/partials/submission-info.php new file mode 100644 index 0000000..f770ed1 --- /dev/null +++ b/lib/partials/submission-info.php @@ -0,0 +1,33 @@ +prepare( + "SELECT round_number, score, comment FROM assignments WHERE completed IS NOT NULL AND submission_id = :submission_id" +); +$stmt_fdbck->execute([ + "submission_id" => $submission->id, +]); +$feedback = $stmt_fdbck->fetchAll(PDO::FETCH_OBJ); +?> +
+ + + + + + + + + + + + + + + + + + + + + +
Submission details
Titletitle ?>
RankingRound Onerank_round_1 ?>
Round Tworank_round_2 ?>
Round Threerank_round_3 ?>
+
diff --git a/www/account/edit.php b/www/account/edit.php new file mode 100644 index 0000000..1d0c84a --- /dev/null +++ b/www/account/edit.php @@ -0,0 +1,225 @@ +handle !== $_POST["handle"]) { + $stmt = $db["data"]->prepare( + "SELECT COUNT(*) FROM members WHERE UPPER(handle) = UPPER(:handle)" + ); + $stmt->execute([ + "handle" => $data["handle"], + ]); + + if ($stmt->fetch(PDO::FETCH_COLUMN) > 0) { + $errors["handle"] = "That handle is taken."; + } + } + + if ($_SESSION["account"]->email !== $_POST["email"]) { + $stmt = $db["data"]->prepare( + "SELECT COUNT(*) FROM members WHERE email = :email" + ); + $stmt->execute([ + "email" => $data["email"], + ]); + + if ($stmt->fetch(PDO::FETCH_COLUMN) > 0) { + $errors["email"] = "That email address is already in use."; + } + } + + if ( + $_POST["password"] && + $_POST["new-password"] && + !password_check($_SESSION["account"]) + ) { + $errors["password"] = "Your password is incorrect."; + } + + if ($_POST["password"] && mb_strlen(trim($_POST["new-password"])) === 0) { + $errors["new-password"] = "You can't have an empty password."; + } + + if ( + $_POST["password"] && + $_POST["new-password"] && + $_POST["new-password"] !== $_POST["new-password-confirm"] + ) { + $errors["new-password"] = "The newly-entered passwords do not match."; + } + + return $errors; +} + +if ($_SERVER["REQUEST_METHOD"] === "POST"): + $errors = validate_fields($_POST); + + if ( + !isset($errors["name"]) && + !isset($errors["handle"]) && + !isset($errors["biography"]) + ) { + $stmt = $db["data"]->prepare( + "UPDATE members SET (name, handle, biography) = (:name, :handle, :biography) WHERE id = :id" + ); + + $stmt->execute([ + "id" => $_SESSION["account"]->id, + "name" => $_POST["name"], + "handle" => $_POST["handle"], + "biography" => $_POST["biography"] ?? null, + ]); + } + + if ( + ($_SESSION["account"]->email !== $_POST["email"] || + $_POST["password"]) && + !isset($errors["email"]) && + !isset($errors["new-password"]) && + !isset($errors["new-password"]) + ) { + $stmt = $db["data"]->prepare( + "UPDATE members SET (email, password) = (:email, :password) WHERE id = :id" + ); + + $password = $_POST["new-password"] + ? password_hash($_POST["new-password"], PASSWORD_ARGON2ID) + : $_SESSION["account"]->password; + + $stmt->execute([ + "id" => $_SESSION["account"]->id, + "email" => $_POST["email"], + "password" => $password, + ]); + } + + $stmt = $db["data"]->prepare("SELECT * FROM members WHERE id = :id"); + + $results = $stmt->execute([ + "id" => $_SESSION["account"]->id, + ]); + + $_SESSION["account"] = $stmt->fetch(PDO::FETCH_OBJ); + + if (count($errors) > 0) { + http_response_code(400); + } else { + $_SESSION["profile_updated"] = true; + http_response_code(303); + header("Location: /account/edit"); + die(); + } +endif; + +include "partials/head.php"; +?> + + +
+
+

+
+ +

You must log in to view this page.

+ + + + + + +

The ability to upload a photo and add links to your profile will return soon; your existing photo and links are still visible.

+

View your profile

+
+
+ Personal Details + + + +
+
+ Account Security + +
+ Change Password + + + +
+
+ +
+ +
+ diff --git a/www/account/index.php b/www/account/index.php new file mode 100644 index 0000000..06a20e1 --- /dev/null +++ b/www/account/index.php @@ -0,0 +1,23 @@ + + + +
+
+

+
+ +

You must log in to view this page.

+ + + +
+ diff --git a/www/account/logout.php b/www/account/logout.php new file mode 100644 index 0000000..31d125d --- /dev/null +++ b/www/account/logout.php @@ -0,0 +1,8 @@ +header { + margin: 0 auto; + max-width: 60rem; +} + +/* Avoid orphans in headings */ +h1, +h2, +h3, +h4 { + text-wrap: balance; +} + +a { + text-decoration-skip-ink: auto; +} + +/* Make images easier to work with */ +img, +picture { + max-width: 100%; + height: auto; + display: block; +} + +/* Inherit fonts for inputs and buttons */ +input, +button, +textarea, +select { + font-family: inherit; + font-size: inherit; +} + +/* Anything that has been anchored to should have extra scroll margin */ +:target { + scroll-margin-block: 1ex; +} + +/* ====== START good content ====== */ + +/* @link https://utopia.fyi/type/calculator?c=320,16,1.2,1440,20,1.25,6,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12 */ + +:root { + /* Step -2: 11.1111px → 12.8px */ + --step--2: clamp(0.6944rem, 0.6643rem + 0.1508vw, 0.8rem); + /* Step -1: 13.3333px → 16px */ + --step--1: clamp(0.8333rem, 0.7857rem + 0.2381vw, 1rem); + /* Step 0: 16px → 20px */ + --step-0: clamp(1rem, 0.9286rem + 0.3571vw, 1.25rem); + /* Step 1: 19.2px → 25px */ + --step-1: clamp(1.2rem, 1.0964rem + 0.5179vw, 1.5625rem); + /* Step 2: 23.04px → 31.25px */ + --step-2: clamp(1.44rem, 1.2934rem + 0.733vw, 1.9531rem); + /* Step 3: 27.648px → 39.0625px */ + --step-3: clamp(1.728rem, 1.5242rem + 1.0192vw, 2.4414rem); + /* Step 4: 33.1776px → 48.8281px */ + --step-4: clamp(2.0736rem, 1.7941rem + 1.3974vw, 3.0518rem); + /* Step 5: 39.8131px → 61.0352px */ + --step-5: clamp(2.4883rem, 2.1094rem + 1.8948vw, 3.8147rem); + /* Step 6: 47.7757px → 76.2939px */ + --step-6: clamp(2.986rem, 2.4767rem + 2.5463vw, 4.7684rem); +} + +.flow>*+* { + margin-block-start: var(--flow-space, 1em); +} + +blockquote { + padding-inline-start: 1em; + border-inline-start: 0.3em solid; + font-style: italic; + font-size: var(--step-1); +} + +h1 { + font-size: var(--step-4); +} + +h2 { + font-size: var(--step-3); +} + +h3 { + font-size: var(--step-2); +} + +:is(h1, h2, h3, blockquote) { + --flow-space: 1.5em; +} + +:is(h1, h2, h3)+* { + --flow-space: 0.5em; +} + + +main>* { + max-width: 60rem; + margin-inline: auto; +} + +a { + text-underline-offset: 0.3ex; + position: relative; +} + +a:hover { + text-decoration-thickness: 0.3ex; +} + +a:active { + text-decoration-thickness: 0.1ex; +} + +/* ====== START project styles ====== */ +:root { + --color-background: linen; + --color-foreground: black; + --color-accent: darkolivegreen; + --color-link: midnightblue; + --color-link-visited: rebeccapurple; + --color-link-active: darkred; + --color-mark: coral; + --color-mark: lightsalmon; + + color-scheme: light; + accent-color: darkolivegreen; + + --font-sans-serif: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; + --font-serif: Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; + --font-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; +} + +:focus { + outline: 0.125rem dashed var(--color-foreground); + outline-offset: 0.125em; +} + +html { + box-sizing: inherit; + word-wrap: break-word; + -ms-hyphens: auto; + hyphens: auto; +} + +body { + padding: 0 2rem; + background-color: var(--color-background); + color: var(--color-foreground); + font-family: var(--font-serif); + font-size: var(--step-0); +} + +a { + color: var(--color-link); +} + +a:visited { + color: var(--color-link-visited); +} + +a:active { + color: var(--color-link-active); +} + +[aria-current="page"] { + border-block-end-color: var(--color-accent); +} + +mark { + padding: 0 0.125em; + background-color: var(--color-mark); + color: black; +} + +body>header { + padding: 2rem 0; + display: flex; + flex-wrap: wrap; + justify-content: space-around; + gap: 1.5rem; +} + +body>header:nth-child(2) a:first-child { + width: 15rem; + height: auto; +} + +body>header nav { + margin: auto 0; + font-size: 1.375rem; +} + +body>header ul { + margin: 0; + padding: 0; + vertical-align: middle; +} + +body>header li, +body>footer li { + display: inline; + vertical-align: middle; +} + +body>header li+li::before { + content: "•"; + font-size: 1em; + margin-left: 0.1875em; + margin-right: 0.1875em; + position: relative; + top: -0.0625em; +} + +body>header a { + text-decoration: none; + color: currentColor !important; + border: 0.25em solid transparent; +} + +body>header a:hover { + border-block-end-color: var(--color-accent); + ; +} + +hr { + margin: 1.5em auto; + width: 50%; + border-color: var(--color-foreground); +} + +dt { + font-weight: 700; +} + +aside.alert, +[data-game-status="0"] [data-status="0"], +[data-game-status="1"] [data-status="1"], +[data-game-status="2"] [data-status="2"], +[data-game-status="3"] [data-status="3"], +[data-game-status="7"] [data-status="7"], +[data-game-status="8"] [data-status="8"], +[data-game-status="9"] [data-status="9"] { + padding: 1em 2em; + border: 0.125em solid currentColor; + border-radius: 0.25em; + background-color: var(--color-mark, Mark); + color: MarkText; +} + +a.call-to-action { + color: black !important; + background: gainsboro; + padding: 0.125em 0.375em; + display: inline-block; + font-variant: small-caps; + text-decoration: none; + border: 0.0625em solid currentColor; + font-size: 0.9em; +} + +a.call-to-action:hover, +a.call-to-action:focus { + background: yellowgreen; +} + +a.call-to-action::after { + content: " →"; +} + +.sr-only { + border: 0 !important; + clip: rect(1px, 1px, 1px, 1px) !important; + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; + height: 1px !important; + overflow: hidden !important; + margin: -1px !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; + white-space: nowrap !important; +} + +.social-media { + display: flex; + justify-content: center; + gap: 0.5rem; +} + +.social-media a { + display: inline-block; + width: 1.75rem; + height: 1.75rem; + font-size: 0; +} + +.social-media a { + color: inherit; +} + +.social-media a:hover { + color: var(--color-accent); +} + +.social-media a svg+span { + display: none +} + +.social-media a:hover { + transform: scale(1.1); +} + +.social-media a:active { + transform: scale(0.9); +} + +svg { + fill: currentColor; + width: 100%; +} + +nav[aria-labelledby="breadcrumb-heading"] ul { + margin: 0 0 0.5em 0; + padding: 0; +} + +nav[aria-labelledby="breadcrumb-heading"] li { + display: inline-block; +} + +nav[aria-labelledby="breadcrumb-heading"] li::after { + --breadcrumb-arrow-size: .4em; + display: inline-block; + margin: 0 0.125em; + content: ""; + width: 0; + height: 0; + border-top: var(--breadcrumb-arrow-size) solid transparent; + border-bottom: var(--breadcrumb-arrow-size) solid transparent; + border-left: var(--breadcrumb-arrow-size) solid currentColor; +} + +nav[aria-labelledby="breadcrumb-heading"] li:last-child::after { + content: unset; +} + +nav[aria-labelledby="breadcrumb-heading"] a { + color: currentColor; +} + +[aria-label="Skip links"] { + position: absolute; + inset-inline: 0; + inset-block-start: -100%; + padding: 0.5em; + background: Canvas; + box-shadow: 0 0 0.5em 0 CanvasText; + z-index: 10; + font-size: 0.9rem; +} + +[aria-label="Skip links"]:focus-within { + inset-block-start: 0%; +} + +[aria-label="Skip links"] ul { + margin: 0; + padding: 0; +} + +[aria-label="Skip links"] li { + display: inline; +} + +[aria-label="Skip links"] li+li { + margin-inline-start: 0.5em; +} + +[aria-label="Skip links"] a { + color: CanvasText; +} + +footer { + margin: 2rem auto 0 auto; + padding: 1rem 0; + max-width: 75rem; + border-block-start: 0.25rem double currentColor; + font-size: 0.9rem; + text-align: center; + clear: both; +} + +footer ul { + padding: 0; +} + +body>footer nav li+li::before { + content: "•"; + font-size: 1em; + margin-left: 0.375em; + margin-right: 0.375em; + position: relative; + top: 0.0625em; +} + +footer nav a { + color: inherit !important; +} + +footer>p:last-child a { + margin: auto; + display: block; + width: 3rem; + font-size: 0; + color: inherit; +} + +label { + display: block; + font-variant: small-caps; +} + +input[type='radio']+label { + display: initial; +} + +label>span { + margin-block-end: 0.125em; + display: block; +} + +input[type="checkbox"]+span, +input[type="radio"]+span { + vertical-align: middle; + font-variant: normal; + display: inline; +} + +label:has(select) span+span { + display: inline-block; + position: relative; +} + +label:has(select) span+span::after { + display: block; + inset-block-start: 50%; + inset-inline-end: 0.75em; + transform: translateY(-50%); + font-style: normal; + position: absolute; + content: "▼"; + font-size: 0.8rem; + line-height: 1; + z-index: 0; + color: inherit; +} + +select { + padding: .125em 2em .125em .5em; + border: .0625em solid currentColor; + color: CanvasText; + background-color: Canvas; + --webkit-appearance: none; + appearance: none; + border-radius: 0.25em; +} + +input[type='text'], +input[type='password'] { + width: 100%; + max-width: 30ch; +} + +input, +textarea { + padding: .25em .5em; + border: 0.0625em solid currentColor; + color: var(--color-foreground); + border-radius: 0.25em; +} + +textarea { + width: 100%; + resize: vertical; + min-height: 10ch; + border-radius: 0; +} + +button { + padding: .375em .625em; + background-color: var(--color-accent, ButtonFace); + color: var(--color-background, ButtonText); + border-radius: 0 0.25em 0 0.25em; + border: 0.0625em solid var(--color-foreground, ButtonBorder); + font-variant: small-caps; + box-shadow: 0.1875em 0.1875em 0 0 var(--color-foreground, ButtonText); +} + + +button:focus { + box-shadow: none; +} + +button:active { + position: relative; + inset-inline-start: 0.1875em; + inset-block-start: 0.1875em; + box-shadow: inset 0 0 0.375em black; +} + +button:disabled { + background-color: black; + color: GrayText; + cursor: not-allowed; + text-decoration: line-through; +} + +pre { + overflow: auto; +} + +pre.code { + background-color: CanvasText; + color: Canvas; + padding: 0.375em 0.625em; + border-radius: 0.375em; + border: 0.0625em solid currentColor; + box-shadow: 0 0 0.375em 0.0625em inset currentColor; +} + +code { + background-color: CanvasText; + color: Canvas; + padding: 0.0625em 0.375em; + border-radius: 0.375em; + border: 0.0625em solid currentColor; + white-space: normal; + word-break: break-all; + font-size: 0.9em; +} + +code a { + color: currentColor !important; + text-decoration-color: currentColor; +} + +/* === Tables === */ +tbody a { + color: inherit; +} + +/* Standard Tables */ +table { + margin: 1em -0.125em; + border-collapse: collapse; + border: 0.1875em solid ButtonBorder; + min-width: 40em; + width: 100%; + color: CanvasText; +} + +caption { + text-align: left; + font-style: italic; + padding: 0.5em; +} + +th, +td { + padding: 0.25em 0.5em 0.25em 1em; + vertical-align: text-top; + text-align: left; + text-indent: -0.5em; +} + +tr { + color: inherit; + border-block-start: 0.0625em solid GrayText; +} + +tbody tr:last-child { + border-block-end: 0.0625em solid GrayText; +} + +th+th, +th+td, +td+td { + border-inline-start: 0.0625em solid GrayText; +} + +caption { + background-color: Canvas; + color: CanvasText; +} + +th[scope="col"] { + vertical-align: bottom; +} + +tbody tr:nth-child(even) { + background-color: rgba(255, 255, 255, 0.2); +} + +tfoot tr, +tfoot th[scope="row"] { + background-color: CanvasText; + color: Canvas; +} + +tfoot td { + border-color: Canvas; +} + + +/* Responsive Tables with Sticky Headers +Via Adrian Roselli +https://adrianroselli.com/2020/11/under-engineered-responsive-tables.html +https://adrianroselli.com/2020/01/fixed-table-headers.html +*/ +[role="region"][aria-labelledby][tabindex] { + overflow: auto; + border: 0.1em solid GrayText; +} + +[role="region"][aria-labelledby][tabindex]:focus { + box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.5); + outline: 0.1em solid rgba(0, 0, 0, 0.1); +} + +[role="region"][aria-labelledby][tabindex] table { + margin: 0; + border: none; +} + +/* Scrolling Visual Cue */ +[role="region"][aria-labelledby][tabindex] { + background: linear-gradient(to right, Canvas 30%, rgba(255, 255, 255, 0)), linear-gradient(to right, rgba(255, 255, 255, 0), Canvas 70%) 0 100%, radial-gradient(farthest-side at 0% 50%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)), radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) 0 100%; + background-repeat: no-repeat; + background-color: Canvas; + background-size: 40px 100%, 40px 100%, 14px 100%, 14px 100%; + background-position: 0 0, 100%, 0 0, 100%; + background-attachment: local, local, scroll, scroll; +} + +/* Fixed Headers */ +th { + position: sticky; + inset-block-start: 0; + z-index: 2; + background-color: Canvas; + color: CanvasText; +} + +th[scope="row"] { + position: sticky; + inset-inline-start: 0; + z-index: 1; + vertical-align: top; +} + +th[scope="row"]::after { + content: ""; + position: absolute; + inset-inline-end: -0.0625em; + inset-block: 0; + border-inline-end: 0.0625em solid GrayText; +} + +th[scope="col"]:first-child::after { + content: ""; + position: absolute; + inset-inline-end: -0.0625em; + inset-block: 0; + border-inline-end: 0.0625em solid GrayText; +} + +th:not([scope="row"]):first-child { + /* left: 0; */ + inset-inline-start: 0; + z-index: 3; +} + +/* ====== page-specific styles ====== */ +.highlight { + display: flex; + flex-wrap: wrap; + gap: 1.5em; + max-width: unset; + background-color: var(--color-mark); + color: black; + position: relative; + margin-inline-start: -2rem; + padding: 3rem; + width: calc(100% + 4rem); + box-shadow: 0 0 0.5em 0.125em currentColor inset; +} + +.highlight a { + color: inherit; +} + +.highlight>div { + flex: 1 1 15rem; +} + +.highlight>div:first-child p { + font-size: var(--step-2); + max-width: 40ch; +} + +#recent-issues { + flex-basis: 15rem; + text-align: center; +} + +#recent-issues>*+* { + margin-block-start: 1em; +} + +#recent-issues>div:first-of-type { + position: relative; +} + +#recent-issues>div picture { + display: inline-block; + position: relative; +} + +#recent-issues>div picture:first-child { + transform: rotate(-6deg) translateX(-50%); + z-index: 20; + position: absolute; + inset-inline-start: 42%; + inset-block-end: 1em; +} + +#recent-issues>div picture:nth-child(2) { + z-index: 10; + inset-block-end: 0.25em; + transform: rotate(3deg); +} + +#recent-issues>div picture:nth-child(3) { + position: absolute; + inset-inline-start: 50%; + transform: rotate(9deg); +} + +#recent-issues>p:first-of-type { + margin-block-start: 0.5em; +} + +[aria-labelledby="swatches-caption"] td { + width: 5rem; + height: 5rem; + background-color: currentColor; + font-size: 0; +} + +#masthead ul span { + font-weight: bold; +} + +.patrons { + padding: 0; + list-style: none; + columns: 3; + font-style: italic; +} + +.cover { + border: 0.0625em solid GrayText; + border-radius: 0.1875em; + box-shadow: 0 0 1em -0.5em CanvasText; + background-color: var(--color-background); + transition: transform 0.375s; +} + +a .cover:hover { + transform: scale(1.03); +} + +a .cover:active { + transform: scale(1); +} + +#issues ol { + padding: 0; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 2em; +} + +#issues li { + text-align: center; +} + +#issues li>a { + display: inline-block; + width: 100%; +} + +#issues p a { + text-decoration: none; + font-weight: 700; + color: inherit; +} + +@supports (display: grid) { + #issues ol { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); + gap: 2em; + } +} + +#issues li.legacy-issue>a { + padding-block-start: 3em; +} + +#issues .legacy-issue picture:first-child { + width: 60%; + position: relative; + inset-block-start: -3em; +} + +#issues .legacy-issue picture:nth-child(2) { + position: absolute; + width: 60%; + inset-block-end: 0; + inset-inline-end: 0; +} + +.issue>header~div, +.issue-legacy>header~div>div { + display: flex; + flex-wrap: wrap; + gap: 1.25em; +} + +.issue-legacy>header~div>div { + gap: 1em; + justify-content: space-between; +} + +.issue>header~div>div h2 { + flex-basis: 100%; +} + +#toc dd { + font-style: italic; +} + +.step-by-step-guide #toc~section { + counter-increment: item; +} + +.step-by-step-guide #toc~section h2::before { + content: counter(item) "."; + margin-inline-end: 0.25em; + font-size: 0.625em; +} diff --git a/www/assets/css/dark-mode.css b/www/assets/css/dark-mode.css new file mode 100644 index 0000000..0d4e981 --- /dev/null +++ b/www/assets/css/dark-mode.css @@ -0,0 +1,12 @@ +:root { + --color-background: #3a3b3f; + --color-foreground: seashell; + --color-accent: yellowgreen; + --color-link: lightskyblue; + --color-link-visited: pink; + --color-link-active: peachpuff; + --color-mark: lightgoldenrodyellow; + + color-scheme: dark; + accent-color: yellowgreen; +} diff --git a/www/assets/js/paypal.js b/www/assets/js/paypal.js new file mode 100644 index 0000000..dc79323 --- /dev/null +++ b/www/assets/js/paypal.js @@ -0,0 +1,130 @@ +{ + const paypalErrors = document.querySelector("#paypal-errors"); + const buttonsContainer = document.getElementById('paypal-button-container'); + + + function resultMessage(msg) { + paypalErrors.innerHTML = `${msg}`; + } + + const GAME_NAME = document.querySelector('input[id="game-name"]').value; + // const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + + window.paypal + .Buttons({ + style: { + shape: "rect", + layout: "vertical", + color: "gold", + label: "paypal", + }, + onInit: function (data, actions) { + buttonsContainer.style.display = 'block'; + }, + async createOrder() { + paypalErrors.textContent = ''; + try { + const response = await fetch("/api/orders", { + method: "POST", + headers: { + "Content-Type": "application/json", + // 'X-CSRF-TOKEN': CSRF_TOKEN, + }, + // use the "body" param to optionally pass additional order information + // like product ids and quantities + body: JSON.stringify({ + payee: { + merchant_id: "YUD9N2F66QX7A", + }, + cart: [{ + description: GAME_NAME, + quantity: "1", + category: "DIGITAL_GOODS", + unit_amount: { + currency_code: "USD", + value: "5.00" + }, + },], + }), + }); + + const orderData = await response.json(); + + if (orderData.id) { + return orderData.id; + } + const errorDetail = orderData?.details?.[0]; + const errorMessage = errorDetail ? + `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})` : + JSON.stringify(orderData); + + throw new Error(errorMessage); + } catch (error) { + console.error(error); + resultMessage(`Could not initiate PayPal Checkout.`); + } + }, + async onApprove(data, actions) { + try { + const response = await fetch(`/api/orders/${data.orderID}/capture`, { + method: "POST", + headers: { + // 'X-CSRF-TOKEN': CSRF_TOKEN, + }, + }); + + const orderData = await response.json(); + // Three cases to handle: + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // (2) Other non-recoverable errors -> Show a failure message + // (3) Successful transaction -> Show confirmation or thank you message + + const errorDetail = orderData?.details?.[0]; + + if (errorDetail?.issue === "INSTRUMENT_DECLINED") { + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // recoverable state, per + // https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/ + return actions.restart(); + } else if (errorDetail) { + // (2) Other non-recoverable errors -> Show a failure message + throw new Error(`${errorDetail.description} (${orderData.debug_id})`); + } else if (!orderData.purchase_units) { + throw new Error(JSON.stringify(orderData)); + } else { + // (3) Successful transaction -> Show confirmation or thank you message + // Or go to another URL: actions.redirect('thank_you.html'); + const transaction = + orderData?.purchase_units?.[0]?.payments?.captures?.[0] || + orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; + resultMessage( + `Payment successful! (Transaction ID: ${transaction.id})` + ); + + document.querySelector("input[name='tx-id']").removeAttribute('disabled'); + document.querySelector("input[name='tx-id']").value = transaction.id; + buttonsContainer.style.display = "none"; + // console.log( + // "Capture result", + // orderData, + // JSON.stringify(orderData, null, 2) + // ); + } + } catch (error) { + console.error(error); + resultMessage( + `Your transaction (Transaction ID: ${transaction.id}) could not be processed.` + ); + } + }, + onError: (err) => { + if (err === "Error: Detected popup close") { return; }; + console.error(err); + }, + onCancel: (data) => { + // Show a cancel page or return to cart + resultMessage("Payment cancelled."); + }, + }) + .render("#paypal-button-container"); +} diff --git a/www/assets/lockup.svg b/www/assets/lockup.svg new file mode 100644 index 0000000..c1da72f --- /dev/null +++ b/www/assets/lockup.svg @@ -0,0 +1 @@ +Sixfold diff --git a/www/assets/sixfold.svg b/www/assets/sixfold.svg new file mode 100644 index 0000000..d9566b3 --- /dev/null +++ b/www/assets/sixfold.svg @@ -0,0 +1,8 @@ + + + The Sixfold logo. + The logo portrays the outline of three concentric hexagons that decrease in width, alternating between black and white. A six-point asterisk with rounded edges sits at the center. + + + + diff --git a/www/docs/index.php b/www/docs/index.php new file mode 100644 index 0000000..cd1e288 --- /dev/null +++ b/www/docs/index.php @@ -0,0 +1,69 @@ +prepare($sql); + $stmt->execute([ + "hash" => $_GET["hash"], + ]); + + $doc = $stmt->fetch(PDO::FETCH_OBJ); + + $sql = "SELECT +member_id +FROM assignments +WHERE submission_id = :submission_id +"; + + $stmt = $db["data"]->prepare($sql); + $stmt->execute([ + "submission_id" => $doc->id, + ]); + + $doc->readers = $stmt->fetchAll(PDO::FETCH_COLUMN); + + $IS_OWNER = LOGGED_IN ? $_SESSION["account"]->id === $doc->owner : false; + $IS_READER = LOGGED_IN ? in_array($_SESSION["account"]->id, $doc->readers, true) : false; + + if ($IS_OWNER || $IS_READER || IS_ADMIN || $doc->is_public) : + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; filename="' . slugify($doc->title) . '.pdf"'); + + echo file_get_contents(sprintf('%s/assets/docs/%s/%s', ABS_PATH, $doc->game_id, $doc->basename)); + die; + endif; + +else: + include "partials/head.php"; ?> + + +
+
+

+
+ is_public): ?> +

You must log in to access this page.

+ + +

Read a random document

+ +
+ + diff --git a/www/docs/random.php b/www/docs/random.php new file mode 100644 index 0000000..fe89034 --- /dev/null +++ b/www/docs/random.php @@ -0,0 +1,12 @@ +query($sql)->fetch(PDO::FETCH_COLUMN); + +http_response_code(303); +header('Location: /docs/' . $hash); +die; diff --git a/www/errors/404.php b/www/errors/404.php new file mode 100644 index 0000000..fedc9c6 --- /dev/null +++ b/www/errors/404.php @@ -0,0 +1,16 @@ + + + +
+
+

+
+

The page you requested could not be found.

+
+ diff --git a/www/errors/503.php b/www/errors/503.php new file mode 100644 index 0000000..43b01ae --- /dev/null +++ b/www/errors/503.php @@ -0,0 +1,18 @@ +header = []; + +include "partials/head.php"; +?> + + +
+
+

+
+

We're doing some maintenance right now; we'll be back shortly. In the meantime, feel free to read our previous issues.

+
+ diff --git a/www/forgot-password.php b/www/forgot-password.php new file mode 100644 index 0000000..01076ee --- /dev/null +++ b/www/forgot-password.php @@ -0,0 +1,182 @@ +prepare( + "SELECT member_id FROM password_resets WHERE hash = :hash" + ); + $stmt->execute([ + "hash" => $_GET["hash"] ?? null, + ]); + + $is_valid_hash = $stmt->fetch(PDO::FETCH_COLUMN) ? true : false; + + if (!$is_valid_hash) { + $_SESSION["alert_message"] = + "This password reset link is expired or invalid."; + } +} + +if ($_SERVER["REQUEST_METHOD"] === "POST"): + define( + "IS_RESET_REQUEST", + $_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["email"]) + ); + define( + "IS_NEW_PASSWORD", + $_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["new-password"]) + ); + + $errors = []; + + if (IS_NEW_PASSWORD) { + if (mb_strlen(trim($_POST["new-password"])) === 0) { + $errors["new-password"] = "You can't have an empty password."; + } + + if ($_POST["new-password"] !== $_POST["new-password-confirm"]) { + $errors["new-password"] = + "The newly-entered passwords do not match."; + } + + if (count($errors) === 0) { + // get id from has lookup + $stmt = $db["data"]->prepare( + "SELECT member_id FROM password_resets WHERE hash = :hash" + ); + $stmt->execute([ + "hash" => $_POST["hash"], + ]); + + $member_id = $stmt->fetch(PDO::FETCH_COLUMN); + + // update password + $stmt = $db["data"]->prepare( + "UPDATE members SET password = :password WHERE id = :id;" + ); + + $stmt->execute([ + "id" => $member_id, + "password" => password_hash( + $_POST["new-password"], + PASSWORD_ARGON2ID + ), + ]); + + $stmt = $db["data"]->prepare( + "DELETE FROM password_resets WHERE hash = :hash;" + ); + + $stmt->execute([ + "hash" => $_POST["hash"], + ]); + + $_SESSION["alert_message"] = + "Your password has been successfully reset."; + + http_response_code(303); + header("Location: /login"); + die(); + } + } + + if (IS_RESET_REQUEST) { + if (mb_strlen(trim($_POST["email"])) === 0) { + $errors["email"] = "Please enter an email address."; + } else { + $stmt = $db["data"]->prepare( + "SELECT id FROM members WHERE email = :email" + ); + $stmt->execute([ + "email" => $_POST["email"], + ]); + $member_id = $stmt->fetch(PDO::FETCH_COLUMN); + + if ($member_id) { + $stmt = $db["data"]->prepare( + "INSERT OR REPLACE INTO password_resets (hash, member_id) VALUES (:hash, :member_id)" + ); + $stmt->execute([ + "hash" => bin2hex(random_bytes(32)), + "member_id" => $member_id, + ]); + + // send password reset + + // $_SESSION["alert_message"] = + // "A password reset email will be sent if an account with the provided email exists."; + http_response_code(303); + header("Location: /forgot-password"); + die(); + } + } + } +endif; + +include "partials/head.php"; +?> + + +
+
+

+
+ + + +
" method="post" class="flow"> + +

+ + + + + + + + + +
+
+ diff --git a/www/games/game.php b/www/games/game.php new file mode 100644 index 0000000..ef41890 --- /dev/null +++ b/www/games/game.php @@ -0,0 +1,208 @@ +prepare($sql); + $stmt->execute([ + "id" => $_GET["game"], + ]); + + $game = $stmt->fetch(PDO::FETCH_OBJ); + unset($stmt); + + $title = "Game: " . $game->name; + $description = "View details about the " . $game->name . " vote."; +} + +include "partials/head.php"; +?> + + +
+
+

+
+ prepare( + "SELECT * FROM assignments WHERE assignments.game_id = :game_id" + ); + $stmt->execute([ + "game_id" => $_GET["game"], + ]); + $assignments = $stmt->execute(); + + $stmt = $db["data"]->prepare( + "SELECT * FROM submissions WHERE game_id = :game_id AND member_id = :member_id" + ); + $stmt->execute([ + "game_id" => $_GET["game"], + "member_id" => $_SESSION["account"]->id, + ]); + $submission = $stmt->fetch(PDO::FETCH_OBJ); + unset($stmt); + + if ($game->status_id === STATUS_ENROLLING && $submission) { + $participant_state = 'GAME_OPEN_WITH_SUBMISSION'; + } elseif ($game->status_id === STATUS_ENROLLING && !$submission) { + $participant_state = 'GAME_OPEN_WITHOUT_SUBMISSION'; + } elseif ($submission && in_array($game->status_id, [ + STATUS_ROUND_ONE, + STATUS_ROUND_TWO, + STATUS_ROUND_THREE, + STATUS_DONE, + ])) { + $participant_state = 'GAME_CLOSED_WITH_SUBMISSION'; + } else { + $participant_state = 'GAME_CLOSED_WITHOUT_SUBMISSION'; + } + + $dates = [ + "submitstart" => DateTimeImmutable::createFromFormat( + "U", + $game->submitstart + )->setTimezone($time_zone), + "submitend" => DateTimeImmutable::createFromFormat( + "U", + $game->submitend + )->setTimezone($time_zone), + "onestart" => DateTimeImmutable::createFromFormat( + "U", + $game->onestart + )->setTimezone($time_zone), + "twostart" => DateTimeImmutable::createFromFormat( + "U", + $game->twostart + )->setTimezone($time_zone), + "threestart" => DateTimeImmutable::createFromFormat( + "U", + $game->threestart + )->setTimezone($time_zone), + "gameend" => DateTimeImmutable::createFromFormat( + "U", + $game->gameend + )->setTimezone($time_zone), + ]; + ?> +
+
+

Your Submission

+ +

title ?>

+

Update submission

+ +

You haven't submitted work to this contest.

+

Submit to name ?>

+ + +

Update submission visibility

+ + + +

You didn't submit a work to this contest.

+ + +
+
+
+

Schedule

+
+
+
Submissions
+
Open:
+
Close:
+
+
+
Document Review
+
Start:
+
End:
+
+
+
Round One
+
Start:
+
End:
+
+
+
Round Two
+
Start:
+
End:
+
+
+
Round Three
+
Start:
+
End:
+
+
+
+
+ +
+ diff --git a/www/games/index.php b/www/games/index.php new file mode 100644 index 0000000..22bddd8 --- /dev/null +++ b/www/games/index.php @@ -0,0 +1,118 @@ +prepare($sql); + $stmt->execute([ + "id" => $_GET["game"], + ]); + $game = $stmt->fetch(); + unset($stmt); + + $title = "Game: " . $game["name"]; + $description = "View details about the " . $game["name"] . " vote."; +} + +include "partials/head.php"; +?> + + +
+
+

+
+
+ prepare($sql); + $stmt->execute(); + + while ($game = $stmt->fetch()) { + static $one_second = new DateInterval("PT1S"); + static $time_zone = new DateTimeZone("America/New_York"); + + $stmt2 = $db["data"]->prepare('SELECT rank_round_1 AS one, rank_round_2 AS two, rank_round_3 AS three, rank_final AS final FROM submissions + WHERE game_id = :game_id AND member_id = :member_id'); + $stmt2->execute([ + 'game_id' => $game['id'], + 'member_id' => $_SESSION['account']->id, + ]); + $ranks = $stmt2->fetch(PDO::FETCH_OBJ); + + $dates = [ + "submitstart" => DateTimeImmutable::createFromFormat( + "U", + $game["submitstart"] + )->setTimezone($time_zone), + "submitend" => DateTimeImmutable::createFromFormat( + "U", + $game["submitend"] + )->setTimezone($time_zone), + "onestart" => DateTimeImmutable::createFromFormat( + "U", + $game["onestart"] + )->setTimezone($time_zone), + "twostart" => DateTimeImmutable::createFromFormat( + "U", + $game["twostart"] + )->setTimezone($time_zone), + "threestart" => DateTimeImmutable::createFromFormat( + "U", + $game["threestart"] + )->setTimezone($time_zone), + "gameend" => DateTimeImmutable::createFromFormat( + "U", + $game["gameend"] + )->setTimezone($time_zone), + ]; + ?> + + +
+
+ diff --git a/www/games/process-order.php b/www/games/process-order.php new file mode 100644 index 0000000..61156cb --- /dev/null +++ b/www/games/process-order.php @@ -0,0 +1,208 @@ + "client_credentials", + ]; + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload)); + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Content-Type: application/x-www-form-urlencoded", + "Authorization: Basic " . + base64_encode(PAYPAL_CLIENT_ID . ":" . PAYPAL_CLIENT_SECRET), + // Uncomment one of these to force an error for negative testing (in sandbox mode only). + ]); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($ch); + $response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + // Further processing ... + if ($response_code === 200) { + return json_decode($response)->access_token; + } else { + header("Content-Type: application/json"); + http_response_code($response_code); + echo $response; + die(); + } +} + +/** + * Create an order to start the transaction. + * + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + * @param array $cart + * @return array + */ +function createOrder($cart) +{ + try { + $access_token = generateAccessToken(); + if (!$access_token) { + http_response_code(500); + header("Content-Type: application/json"); + echo json_encode(["error" => "Failed to obtain access token."]); + die(); + } + + $url = PAYPAL_BASE_URL . "/v2/checkout/orders"; + + $payload = [ + "intent" => "CAPTURE", + "purchase_units" => [ + [ + "amount" => [ + "currency_code" => $cart[0]->unit_amount->currency_code, + "value" => $cart[0]->unit_amount->value, + "breakdown" => [ + "item_total" => [ + "currency_code" => + $cart[0]->unit_amount->currency_code, + "value" => $cart[0]->unit_amount->value, + ], + ], + ], + "items" => [ + [ + "name" => $cart[0]->description, + "description" => $cart[0]->description, + "unit_amount" => [ + "currency_code" => + $cart[0]->unit_amount->currency_code, + "value" => $cart[0]->unit_amount->value, + ], + "quantity" => 1, + ], + ], + ], + ], + ]; + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Content-Type: application/json", + "Authorization: Bearer $access_token", + ]); + // Uncomment one of these to force an error for negative testing (in sandbox mode only). + // Documentation: https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/ + // 'PayPal-Mock-Response' => '{"mock_application_codes": "MISSING_ + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($ch); + $response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + // Further processing ... + if ($response_code === 200) { + header("Content-Type: application/json"); + http_response_code($response_code); + echo $response; + die(); + } else { + header("Content-Type: application/json"); + http_response_code($response_code); + echo $response; + die(); + } + } catch (\Exception $error) { + header("Content-Type: application/json"); + http_response_code(500); + echo json_encode(["error" => "Failed to create order."]); + die(); + } +} + +/** + * Capture payment for the given order + * + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + * @param array $cart + * @return array + */ +function captureOrder($order_id) +{ + $url = PAYPAL_BASE_URL . "/v2/checkout/orders/{$order_id}/capture"; + // Http::fake(function ($request) { + // // Capture and log request headers + // $headers = $request->headers(); + + // // Log headers for inspection + // \Log::info('Captured Request Headers', $headers); + + // return Http::response('', 200, [ + // 'X-Custom-Response-Header' => 'HeaderValue' + // ]); + // }); + $auth = base64_encode(PAYPAL_CLIENT_ID . ":" . PAYPAL_CLIENT_SECRET); + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Content-Type: application/json", + "Authorization: Basic $auth", + ]); + // Uncomment one of these to force an error for negative testing (in sandbox mode only). + // Documentation: https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/ + // 'PayPal-Mock-Response' => '{"mock_application_codes": "MISSING_ + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($ch); + $response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + // Further processing ... + if ($response_code === 200) { + header("Content-Type: application/json"); + http_response_code($response_code); + echo $response; + die(); + } else { + header("Content-Type: application/json"); + http_response_code($response_code); + echo $response; + die(); + } +} + +if ( + $_SERVER["REQUEST_METHOD"] === "POST" && + $_SERVER["REQUEST_URI"] === "/api/orders" +) { + $body = file_get_contents("php://input"); + $cart = json_decode($body)->cart; + createOrder($cart); +} + +if ($_SERVER["REQUEST_METHOD"] === "POST") { + $order_id = substr($_SERVER["REQUEST_URI"], 12, 17); + captureOrder($order_id); +} + +if ($_SERVER["REQUEST_METHOD"] !== "POST") { + http_response_code(404); + die(); +} diff --git a/www/games/results.php b/www/games/results.php new file mode 100644 index 0000000..5d81e87 --- /dev/null +++ b/www/games/results.php @@ -0,0 +1,140 @@ +prepare("SELECT name FROM games WHERE id = :id"); + +$stmt->execute([ + "id" => $_GET["game"], +]); + +$title = "Results: " . $stmt->fetch(PDO::FETCH_COLUMN); +$description = "View the results of particular issue's voting."; + +$stmt = $db["data"] + ->prepare('SELECT submissions.id AS id, submissions.hash, members.name, members.handle, + CASE WHEN name_is_public = 1 AND name IS NOT NULL + THEN name + ELSE CONCAT("Member ", submissions.member_id) + END AS author, + CASE WHEN doc_is_public = 1 + THEN title + ELSE CONCAT("Entry ", submissions.id) + END AS title + , doc_is_public, name_is_public, score_round_1, rank_round_1, score_round_2, rank_round_2, score_round_3, rank_round_3, rank_final, votes_round_1, votes_round_2, votes_round_3, num_assignments_round_1, num_assignments_round_2, num_assignments_round_3 + FROM submissions + JOIN members ON submissions.member_id = members.id + WHERE submissions.game_id = :id AND rank_final IS NOT NULL AND submissions.transaction_id IS NOT NULL ORDER BY rank_final ASC'); + +$results = $stmt->execute([ + "id" => $_GET["game"], +]); +$row = $stmt->fetch(); + +if (!$row) { + $title = "No Results Found"; + $description = "We couldn't find the game results you requested."; +} + +include "partials/head.php"; +?> + + +
+
+

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + > + + + + + + + + + + fetch()); ?> + + + +
Results for Game
TitleAuthorScoreRank
Round OneRound TwoRound ThreeRound OneRound TwoRound Three
+ $doc_title" + : $doc_title ?> + (disqualified) + $author" + : $author ?>>>>
+
+ +

+ +
+ diff --git a/www/games/submit.php b/www/games/submit.php new file mode 100644 index 0000000..dfa1586 --- /dev/null +++ b/www/games/submit.php @@ -0,0 +1,301 @@ +prepare($sql); + $stmt->execute([ + "game_id" => $_GET["game"], + "member_id" => $_SESSION["account"]->id, + ]); + + $HAS_SUBMISSION = $stmt->fetch(PDO::FETCH_COLUMN) !== false; + if ($HAS_SUBMISSION) { + http_response_code(303); + header("Location: /games/" . $_GET["game"] . "/update"); + die(); + } + + $sql = "SELECT id, name, status_id FROM games + WHERE id = :id"; + + $stmt = $db["data"]->prepare($sql); + $stmt->execute([ + "id" => $_GET["game"], + ]); + $game = $stmt->fetch(PDO::FETCH_OBJ); + + $title = "Submit: {$game->name}"; + $description = "Enter a work into the " . $game->name . " vote."; +} + +if ($_SERVER["REQUEST_METHOD"] === "POST"): + $stmt = $db["data"]->prepare("SELECT id FROM games WHERE id = :id"); + $stmt->execute([ + "id" => $_GET["game"], + ]); + + $game_id = $stmt->fetch(PDO::FETCH_COLUMN) ?? false; + $errors = []; + + $RULES_FOLLOWED = + isset($_POST["agree-toc"]) && + $_POST["agree-toc"] === "1" && + (isset($_POST["agree-guidelines"]) && + $_POST["agree-guidelines"] === "1"); + + if (!$RULES_FOLLOWED) { + http_response_code(400); + $errors["agreements"] = + "Please accept the Terms & Conditions and the Submission Guidelines."; + } + + if ($_FILES["manuscript"]["size"] === 0) { + http_response_code(400); + $errors["filesize"] = "A file upload is required."; + } elseif ($_FILES["manuscript"]["size"] > UPLOAD_MAX_FILESIZE) { + http_response_code(400); + $errors["filesize"] = "Your document is too large."; + } else { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime_type = finfo_file($finfo, $_FILES["manuscript"]["tmp_name"]); + finfo_close($finfo); + + $PROPER_MIMETYPE = $mime_type === "application/pdf"; + } + + if (isset($PROPER_MIMETYPE) && !$PROPER_MIMETYPE) { + http_response_code(400); + $errors["mimetype"] = "Only PDF submissions are allowed."; + } + + if (!isset($_POST["title"]) || !trim($_POST["title"])) { + http_response_code(400); + $errors["title"] = "Please enter a title."; + } + + if (!isset($_SESSION["account"]) && !isset($_SESSION["account"]->id)) { + http_response_code(500); + $errors["account"] = + "We can't upload a document without knowing which account it belongs to."; + } + + if (!$game_id) { + http_response_code(400); + $errors["game"] = "The chosen game doesn't exist."; + } + + if (!isset($_POST["tx-id"])) { + $errors["payment"] = "No transaction ID was provided."; + } + + if (count($errors) > 0) { + if (!isset($errors["filesize"]) && !isset($errors["mimetype"])) { + $errors["upload"] = + "Fix all other errors and choose your file again."; + } + } else { + $new_basename = + md5(microtime() . $game_id . $_SESSION["account"]->id) . ".pdf"; + $lookup_hash = md5( + $_SESSION["account"]->id . $game_id . microtime() . "salt" + ); + + $file_destination = sprintf( + "%s/%s/%s", + DIRECTORY_DOCS, + $game_id, + $new_basename + ); + + try { + $file_moved = move_uploaded_file( + $_FILES["manuscript"]["tmp_name"], + $file_destination + ); + + if ($file_moved) { + $show_doc = isset($_POST["public-doc"]) ? 1 : 0; + $show_name = isset($_POST["public-name"]) ? 1 : 0; + + $stmt = $db["data"] + ->prepare("INSERT INTO submissions (member_id, game_id, title, basename, hash, doc_is_public, name_is_public, transaction_id, status, is_freeroll, created_at) + VALUES (:member_id, :game_id, :title, :basename, :hash, :doc_is_public, :name_is_public, :transaction_id, :status, :is_freeroll, :created_at)"); + + $stmt->execute([ + "member_id" => $_SESSION["account"]->id, + "game_id" => $_GET["game"], + "title" => $_POST["title"], + "basename" => "$new_basename", + "hash" => $lookup_hash, + "doc_is_public" => $show_doc, + "name_is_public" => $show_name, + "transaction_id" => $_POST["tx-id"], + "status" => 1, + "is_freeroll" => 0, + "created_at" => date("Y-m-d\TH:i:s\Z"), + ]); + + http_response_code(303); + header("Location: /games/" . $_GET["game"]); + } + } catch (Exception $e) { + http_response_code(500); + unlink($file_destination); + + $errors["upload"] = + "There was an error adding your submission to our database. Please try again."; + } + } +endif; + +include "partials/head.php"; +?> + + +
+
+

+
+ + + +
" method="post" enctype="multipart/form-data" class="flow"> + +

+

+
+ Manuscript details + + +
+ + + +
+
+
+ Privacy settings + + +
+
+ Payment +
+

Payment is required before a submission will be processed.

+

Payment successful! (Transaction ID: )

+ + +
+
+ + + + +
+ +
+ +
+ diff --git a/www/games/update.php b/www/games/update.php new file mode 100644 index 0000000..be56b31 --- /dev/null +++ b/www/games/update.php @@ -0,0 +1,370 @@ +prepare($sql); + $stmt->execute([ + "game_id" => $_GET["game"], + "member_id" => $_SESSION["account"]->id, + ]); + + $submission = $stmt->fetch(PDO::FETCH_OBJ); + if (!$submission) { + http_response_code(303); + header("Location: /games/" . $_GET["game"]); + die(); + } + + $sql = "SELECT id, name, status_id FROM games + WHERE id = :id"; + + $stmt = $db["data"]->prepare($sql); + $stmt->execute([ + "id" => $_GET["game"], + ]); + $game = $stmt->fetch(PDO::FETCH_OBJ); + + define("GAME_IS_OPEN", $game->status_id === STATUS_ENROLLING); + + $title = "Update: {$game->name}"; + $description = "Update your submission for the " . $game->name . " vote."; +} + +if ($_SERVER["REQUEST_METHOD"] === "POST"): + define("NEW_MANUSCRIPT", $_POST["keep-manuscript"] === "0"); + define("EXISTING_MANUSCRIPT", $_POST["keep-manuscript"] === "1"); + define( + "RULES_WERE_FOLLOWED", + isset($_POST["agree-toc"]) && + $_POST["agree-toc"] === "1" && + (isset($_POST["agree-guidelines"]) && + $_POST["agree-guidelines"] === "1") + ); + define("FILE_EMPTY", $_FILES["manuscript"]["size"] === 0); + define("FILE_TOO_BIG", $_FILES["manuscript"]["size"] > UPLOAD_MAX_FILESIZE); + + $stmt = $db["data"]->prepare("SELECT id FROM games WHERE id = :id"); + $stmt->execute([ + "id" => $_GET["game"], + ]); + + $errors = []; + + if (!$stmt->fetch(PDO::FETCH_COLUMN)) { + $errors["game"] = "The chosen game doesn't exist."; + } + + if (GAME_IS_OPEN && NEW_MANUSCRIPT && !RULES_WERE_FOLLOWED) { + $errors["agreements"] = + "Please accept the Terms & Conditions and the Submission Guidelines."; + } + + if (GAME_IS_OPEN && NEW_MANUSCRIPT && FILE_EMPTY) { + $errors["filesize"] = "A file upload is required."; + } elseif (NEW_MANUSCRIPT && FILE_TOO_BIG) { + $errors["filesize"] = "Your document is too large."; + } elseif (GAME_IS_OPEN && NEW_MANUSCRIPT) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime_type = finfo_file($finfo, $_FILES["manuscript"]["tmp_name"]); + finfo_close($finfo); + + $PROPER_MIMETYPE = $mime_type === "application/pdf"; + } + + if (isset($PROPER_MIMETYPE) && !$PROPER_MIMETYPE) { + $errors["mimetype"] = "Only PDF submissions are allowed."; + } + + if (GAME_IS_OPEN && (!isset($_POST["title"]) || !trim($_POST["title"]))) { + $errors["title"] = "Please enter a title."; + } + + if (GAME_IS_OPEN && !isset($_SESSION["account"])) { + $errors["account"] = + "We can't upload a document without knowing which account it belongs to."; + } + + if (GAME_IS_OPEN && !isset($_POST["tx-id"])) { + $errors["payment"] = "You must submit a payment."; + } + + if (count($errors) > 0) { + http_response_code(400); + define( + "HAS_FILE_ERRORS", + isset($errors["filesize"]) || isset($errors["mimetype"]) + ); + + if (NEW_MANUSCRIPT && !HAS_FILE_ERRORS) { + $errors["upload"] = + "Fix all other errors and choose your file again."; + } + } else { + $params = [ + "submission_id" => $submission->id, + "title" => $_POST["title"], + "doc_is_public" => isset($_POST["public-doc"]) ? 1 : 0, + "name_is_public" => isset($_POST["public-name"]) ? 1 : 0, + ]; + + if (GAME_IS_OPEN && NEW_MANUSCRIPT) { + $basename = + md5(microtime() . $game->id . $_SESSION["account"]->id) . + ".pdf"; + $hash = md5( + $_SESSION["account"]->id . $game->id . microtime() . "salt" + ); + + $file_destination = sprintf( + "%s/%s/%s", + DIRECTORY_DOCS, + $game->id, + $basename + ); + + try { + $file_moved = move_uploaded_file( + $_FILES["manuscript"]["tmp_name"], + $file_destination + ); + + if ($file_moved) { + $stmt = $db["data"]->prepare( + "SELECT basename FROM submissions WHERE id = :submission_id" + ); + $stmt->execute([ + "submission_id" => $submission->id, + ]); + + $old_manuscript = sprintf( + "%s/%s/%s", + DIRECTORY_DOCS, + $game->id, + $stmt->fetch(PDO::FETCH_COLUMN) + ); + unlink($old_manuscript); + + $stmt = $db["data"] + ->prepare("UPDATE submissions SET (title, basename, hash, doc_is_public, name_is_public, created_at) + = (:title, :basename, :hash, :doc_is_public, :name_is_public, :created_at) WHERE id = :submission_id"); + + $params["basename"] = $basename; + $params["hash"] = $hash; + $params["created_at"] = date("Y-m-d\TH:i:s\Z"); + + $stmt->execute($params); + + http_response_code(303); + header("Location: /games/" . $_GET["game"]); + } + } catch (Exception $e) { + var_dump($e); + http_response_code(500); + unlink($file_destination); + + $errors["upload"] = + "There was an error adding your submission to our database. Please try again."; + } + } + + if (GAME_IS_OPEN && EXISTING_MANUSCRIPT) { + $stmt = $db["data"] + ->prepare("UPDATE submissions SET (title, doc_is_public, name_is_public) + = (:title, :doc_is_public, :name_is_public) WHERE id = :submission_id"); + + $stmt->execute($params); + + http_response_code(303); + header("Location: /games/" . $_GET["game"]); + } + if (!GAME_IS_OPEN) { + $stmt = $db["data"] + ->prepare("UPDATE submissions SET (doc_is_public, name_is_public) + = (:doc_is_public, :name_is_public) WHERE id = :submission_id"); + + $stmt->execute([ + "submission_id" => $submission->id, + "doc_is_public" => isset($_POST["public-doc"]) ? 1 : 0, + "name_is_public" => isset($_POST["public-name"]) ? 1 : 0, + ]); + + http_response_code(303); + header("Location: /games/" . $_GET["game"]); + } + } +endif; + +include "partials/head.php"; +?> + + +
+
+

+
+ +

If you would like to withdraw your submission, please email us at sixfold@sixfold.org.

+ +

This game's submissions are now locked, and you may only edit your work's public visibility.

+ +
+ + + + + + + + + + + + + + + + +
Submission Details
Submission IDTransaction IDAccount ID
id ?>transaction_id ?>id ?>
+
+ + + + doc_is_public; + $name_is_public = isset($_POST["public-name"]) + ? (bool) $_POST["public-name"] + : $submission->name_is_public; + ?> +
" method="post" enctype="multipart/form-data" class="flow"> +

+ +
+ Manuscript details + + />
+ /> + +
+ + + +
+
+ +
+ Privacy settings + + +
+ +
+ Payment +
+

You have already paid for this submission. (Transaction ID: transaction_id ?>)

+ +
+ + +
+ +
+ diff --git a/www/index.php b/www/index.php new file mode 100644 index 0000000..4956744 --- /dev/null +++ b/www/index.php @@ -0,0 +1,68 @@ + + + +
+
+

+
+ +

You must log in to view this page.

+ query($sql); + + while ($game = $stmt->fetch()) { + $dates = [ + 'submitstart' => DateTimeImmutable::createFromFormat('U', $game['submitstart'])->setTimezone($time_zone), + 'submitend' => DateTimeImmutable::createFromFormat('U', $game['submitend'])->setTimezone($time_zone), + 'onestart' => DateTimeImmutable::createFromFormat('U', $game['onestart'])->setTimezone($time_zone), + 'twostart' => DateTimeImmutable::createFromFormat('U', $game['twostart'])->setTimezone($time_zone), + 'threestart' => DateTimeImmutable::createFromFormat('U', $game['threestart'])->setTimezone($time_zone), + 'gameend' => DateTimeImmutable::createFromFormat('U', $game['gameend'])->setTimezone($time_zone), + ]; + ?> +
+

+

Status:

+
+ +
Submissions
+
Open:
+
Close:
+ + +
Round One
+
Open:
+
Close:
+ + +
Round Two
+
Open:
+
Close:
+ + +
Round Three
+
Open:
+
Close:
+ + +

Submissions are currently under review.

+

Round One begins on

+ +
+

details

+
+ +

View previously completed games

+ +
+ diff --git a/www/login.php b/www/login.php new file mode 100644 index 0000000..e3a0101 --- /dev/null +++ b/www/login.php @@ -0,0 +1,58 @@ +id)) { + http_response_code(303); + header("Location: /"); + die(); +} + +$description = "Log in to access your account."; +$title = "Login"; + +if ($_SERVER["REQUEST_METHOD"] === "POST"): ?> +prepare("SELECT * FROM members WHERE email = :email"); +$results = $stmt->execute([ + "email" => $_POST["email"], +]); +$account = $stmt->fetch(PDO::FETCH_OBJ); +if (!$account) { + http_response_code(401); +} elseif (password_check($account)) { + $_SESSION["account"] = $account; + http_response_code(303); + header("Location: " . $_POST["redirect"] ?? "/"); + die(); +} +endif; +include "partials/head.php"; +?> + + +
+
+

+
+ + + + + + + + + + + + + +
+ diff --git a/www/members/index.php b/www/members/index.php new file mode 100644 index 0000000..b438381 --- /dev/null +++ b/www/members/index.php @@ -0,0 +1,105 @@ +query('SELECT COUNT(*) FROM members', PDO::FETCH_COLUMN, 0)->fetch(); + +$page = intval($_GET['page'] ?? 1); +$per_page = intval($_GET['per_page'] ?? 50); +$num_pages = ceil($member_count / $per_page) + ($member_count % $per_page > 0 ? 1 : 0); +$has_next_page = $page < $num_pages; + +$sql = "SELECT +id, +name, +email, +handle, +links, +created_at +FROM members +ORDER BY created_at DESC +LIMIT :per_page +OFFSET :offset"; + +$stmt = $db['data']->prepare($sql); +$stmt->execute([ + 'per_page' => ($per_page > 500) ? 500 : $per_page, + 'offset' => ($page - 1) * $per_page, +]); + +include "partials/head.php"; ?> + + +
+
+

+
+ 500) { ?>

Results are limited to 500 per page.

+
+ + + + + + + + + + + fetchAll() as $member) { + // set empty names + // $stmt2 = $db['data']->prepare('UPDATE members SET name = :name WHERE name = " "'); + // $stmt2->execute([ + // 'name' => NULL + // ]); + + // fix broken encodings + // $stmt2 = $db['data']->prepare('UPDATE members SET name = :name WHERE id = :id'); + // $stmt2->execute([ + // 'id' => $member['id'], + // 'name' => mb_convert_encoding($member['name'], 'Windows-1252','utf-8'), + // ]); + + // fix null handles + // $stmt2 = $db['data']->prepare('UPDATE members SET handle = :handle WHERE id = :id'); + // $stmt2->execute([ + // 'id' => $member['id'], + // 'handle' => 'member-' . $member['id'] + // ]); + // + // set empty bio + $stmt2 = $db['data']->prepare('UPDATE members SET biography = :bio WHERE biography = ""'); + $stmt2->execute([ + 'bio' => NULL + ]); + + // fix links + $stmt2 = $db['data']->prepare('UPDATE members SET links = :links WHERE id = :id'); + $stmt2->execute([ + 'links' => str_replace('", url:', '", "url":', $member['links']), + 'id' => $member['id'] + ]); + ?> + + + + + + + + +
Sixfold Member List ( members)
HandleNameDate Joined
format("j F Y") ?>
+
+ +
+ diff --git a/www/members/member.php b/www/members/member.php new file mode 100644 index 0000000..2692db9 --- /dev/null +++ b/www/members/member.php @@ -0,0 +1,69 @@ +prepare($sql); +$stmt->execute([ + 'handle' => $_GET['handle'], +]); +$member = $stmt->fetch(); + +if ($member) { + $name = $member['name'] ?? 'Member ' . $member['id']; + $title = 'Member: ' . $name; + $description = $member['biography'] ?? "No biography provided."; +} + +include "partials/head.php"; ?> + + +
+
+

()

+
+ prepare('UPDATE members SET name = :name WHERE id = :id'); + // $stmt2->execute([ + // 'id' => $member['id'], + // 'name' => mb_convert_encoding($member['name'], 'Windows-1252','utf-8'), + // ]); + + // fix null handles + // $stmt2 = $db['data']->prepare('UPDATE members SET handle = :handle WHERE id = :id'); + // $stmt2->execute([ + // 'id' => $member['id'], + // 'handle' => 'member-' . $member['id'] + // ]); + ?> +
+ +
No biography provided.

"?>
+ + + +
+ +

We couldn't find anyone with the handle .

+ +
+ diff --git a/www/signup.php b/www/signup.php new file mode 100644 index 0000000..b951ada --- /dev/null +++ b/www/signup.php @@ -0,0 +1,125 @@ +prepare("SELECT COUNT(*) FROM members WHERE email = :email"); + $stmt->execute([ + "email" => $data["email"], + ]); + if ($stmt->fetch(PDO::FETCH_COLUMN) > 0) { + $errors["email"] = "That email address is already in use."; + } + + $stmt = $db["data"]->prepare("SELECT COUNT(*) FROM members WHERE UPPER(handle) = UPPER(:handle)"); + $stmt->execute([ + "handle" => $data["handle"], + ]); + if ($stmt->fetch(PDO::FETCH_COLUMN) > 0) { + $errors["handle"] = "That handle is taken."; + } + + if (!isset($data['terms-privacy'])) { + $errors["terms-privacy"] = "Please agree to the terms."; + } + + if (preg_match('/[^A-Za-z0-9-_]/', $_POST['handle'])) { + $errors["handle"] = "Please only use allowed characters."; + } + + return $errors; +} + +$description = "Sign up to submit your work to Sixfold."; +$title = "Sign Up"; + +if ($_SERVER["REQUEST_METHOD"] === "POST"): + $errors = validate_fields($_POST); + + if (count($errors) === 0) { + $stmt = $db["data"] + ->prepare('INSERT INTO members (name, handle, email, password, account_type, created_at, last_access) + VALUES (:name, :handle, :email, :password, :account_type, :created_at, :last_access) + RETURNING id'); + + $stmt->execute([ + "name" => $_POST["name"], + "handle" => $_POST["handle"], + "email" => $_POST["email"], + "password" => password_hash($_POST['password'], PASSWORD_ARGON2ID), + "account_type" => 1, + "created_at" => date("Y-m-dTH:i:s"), + "last_access" => date("Y-m-dTH:i:s"), + ]); + + $stmt = $db['data']->query('SELECT * FROM members WHERE email = :email'); + $stmt->execute([ + 'email' => $_POST['email'], + ]); + + $_SESSION['account'] = $stmt->fetch(PDO::FETCH_OBJ); + } else { + http_response_code(400); + } +endif; + +if (isset($_SESSION["account"])) { + http_response_code(303); + header("Location: /"); + die(); +} + +include "partials/head.php"; +?> + + +
+
+

+
+ +

Your browser is not set to accept cookies. You cannot login or use certain portions of the site unless your browser accepts cookies. Please change your browser settings to accept cookies and try again. Thanks.

+ + + + +

We encounterred an issue when processing your request; please try again.

+
" method="post" class="flow"> +

All fields are required. Pseudonyms are welcome.

+ + + + + + +
+

Log in to an existing account

+
+