This commit is contained in:
Pachira 2024-12-24 10:37:16 +01:00
commit 968305d049
42 changed files with 3067 additions and 0 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/config.php
/.debug
/vendor

6
composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require": {
"swiftmailer/swiftmailer": "^6.0",
"erusev/parsedown": "^1.7"
}
}

481
composer.lock generated Normal file
View file

@ -0,0 +1,481 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "49f50504731fd9419fd191d68c171bd1",
"packages": [
{
"name": "doctrine/lexer",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/lexer.git",
"reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6",
"reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6",
"shasum": ""
},
"require": {
"php": "^7.2"
},
"require-dev": {
"doctrine/coding-standard": "^6.0",
"phpstan/phpstan": "^0.11.8",
"phpunit/phpunit": "^8.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
"homepage": "https://www.doctrine-project.org/projects/lexer.html",
"keywords": [
"annotations",
"docblock",
"lexer",
"parser",
"php"
],
"time": "2019-10-30T14:39:59+00:00"
},
{
"name": "egulias/email-validator",
"version": "2.1.11",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
"reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23",
"reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23",
"shasum": ""
},
"require": {
"doctrine/lexer": "^1.0.1",
"php": ">= 5.5"
},
"require-dev": {
"dominicsayers/isemail": "dev-master",
"phpunit/phpunit": "^4.8.35||^5.7||^6.0",
"satooshi/php-coveralls": "^1.0.1",
"symfony/phpunit-bridge": "^4.4@dev"
},
"suggest": {
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Egulias\\EmailValidator\\": "EmailValidator"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Eduardo Gulias Davis"
}
],
"description": "A library for validating emails against several RFCs",
"homepage": "https://github.com/egulias/EmailValidator",
"keywords": [
"email",
"emailvalidation",
"emailvalidator",
"validation",
"validator"
],
"time": "2019-08-13T17:33:27+00:00"
},
{
"name": "erusev/parsedown",
"version": "1.7.3",
"source": {
"type": "git",
"url": "https://github.com/erusev/parsedown.git",
"reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/erusev/parsedown/zipball/6d893938171a817f4e9bc9e86f2da1e370b7bcd7",
"reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35"
},
"type": "library",
"autoload": {
"psr-0": {
"Parsedown": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Emanuil Rusev",
"email": "hello@erusev.com",
"homepage": "http://erusev.com"
}
],
"description": "Parser for Markdown.",
"homepage": "http://parsedown.org",
"keywords": [
"markdown",
"parser"
],
"time": "2019-03-17T18:48:37+00:00"
},
{
"name": "swiftmailer/swiftmailer",
"version": "v6.2.1",
"source": {
"type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git",
"reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
"reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
"shasum": ""
},
"require": {
"egulias/email-validator": "~2.0",
"php": ">=7.0.0",
"symfony/polyfill-iconv": "^1.0",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"require-dev": {
"mockery/mockery": "~0.9.1",
"symfony/phpunit-bridge": "^3.4.19|^4.1.8"
},
"suggest": {
"ext-intl": "Needed to support internationalized email addresses",
"true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.2-dev"
}
},
"autoload": {
"files": [
"lib/swift_required.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chris Corbyn"
},
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
}
],
"description": "Swiftmailer, free feature-rich PHP mailer",
"homepage": "https://swiftmailer.symfony.com",
"keywords": [
"email",
"mail",
"mailer"
],
"time": "2019-04-21T09:21:45+00:00"
},
{
"name": "symfony/polyfill-iconv",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-iconv.git",
"reference": "685968b11e61a347c18bf25db32effa478be610f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/685968b11e61a347c18bf25db32effa478be610f",
"reference": "685968b11e61a347c18bf25db32effa478be610f",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-iconv": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.12-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Iconv\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Iconv extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"iconv",
"polyfill",
"portable",
"shim"
],
"time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
"reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php72": "^1.9"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.12-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Idn\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Laurent Bassin",
"email": "laurent@bassin.info"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"idn",
"intl",
"polyfill",
"portable",
"shim"
],
"time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17",
"reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.12-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/polyfill-php72",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
"reference": "04ce3335667451138df4307d6a9b61565560199e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e",
"reference": "04ce3335667451138df4307d6a9b61565560199e",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.12-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php72\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"time": "2019-08-06T08:03:45+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

21
config.example.php Normal file
View file

@ -0,0 +1,21 @@
<?php
define('CHIE_DB_DSN', 'mysql:unix_socket=/var/run/mysqld/mysqld.sock;dbname=chie;charset=utf8mb4');
define('CHIE_DB_USER', 'root');
define('CHIE_DB_PASS', '');
define('CHIE_SMTP_HOST', 'smtp.example.com');
define('CHIE_SMTP_PORT', 587);
define('CHIE_SMTP_ENC', 'tls');
define('CHIE_SMTP_USER', 'system@example.com');
define('CHIE_SMTP_PASS', 'toastiscool100');
define('CHIE_CSRF_SECRET', 'some random secret shit');
define('SATORI_HOST', 'localhost');
define('SATORI_PORT', 12345);
define('SATORI_SECRET', 'more secret shit');
define('ANTI_SPAM_KEY', 'secret mewow');
define('ANTI_SPAM_ANSWER', strrev('chie'));
define('GITHUB_SECRET', 'ok secret but long because this one can do bad things');

131
include/_category.php Normal file
View file

@ -0,0 +1,131 @@
<?php
function category_info(int $category): array {
global $pdo;
static $cats = [];
if($category < 1)
return [];
if(!empty($cats[$category]))
return $cats[$category];
$getCat = $pdo->prepare('SELECT * FROM `fmf_categories` WHERE `cat_id` = :id');
$getCat->bindValue('id', $category);
$categoryInfo = $getCat->execute() ? $getCat->fetch(PDO::FETCH_ASSOC) : false;
return $cats[$category] = ($categoryInfo ? $categoryInfo : []);
}
function category_children(int $parent = 0, int $recurse = 3): array {
global $pdo;
if($parent < 0)
return [];
$getCats = $pdo->prepare('SELECT * FROM `fmf_categories` WHERE `cat_parent` = :parent ORDER BY `cat_order`');
$getCats->bindValue('parent', $parent);
$categories = $getCats->execute() ? $getCats->fetchAll(PDO::FETCH_ASSOC) : false;
$categories = $categories ? $categories : [];
if($recurse > 0) {
$recurse--;
for($i = 0; $i < count($categories); $i++)
$categories[$i]['children'] = category_children($categories[$i]['cat_id'], $recurse);
}
return $categories;
}
function root_category(): array {
$categories = category_children();
$forums = [];
for($i = 0; $i < count($categories); $i++) {
if($categories[$i]['cat_type'] != 1) {
$forums[] = $categories[$i];
unset($categories[$i]);
}
}
if(count($forums) > 0)
$categories[] = [
'cat_id' => 0,
'cat_type' => 1,
'cat_name' => 'Categories',
'children' => $forums,
];
return $categories;
}
function category_bump(int $category, ?int $post = null, bool $topics = true, bool $posts = true): void {
global $pdo;
if($category < 1)
return;
$getParentId = $pdo->prepare('SELECT `cat_parent` FROM `fmf_categories` WHERE `cat_id` = :id');
$getParentId->bindValue('id', $category);
$parentId = $getParentId->execute() ? (int)$getParentId->fetchColumn() : 0;
if($parentId > 0)
category_bump($parentId, $post, $topics, $posts);
$bump = $pdo->prepare('UPDATE `fmf_categories` SET `cat_count_topics` = IF(:topics, `cat_count_topics` + 1, `cat_count_topics`), `cat_count_posts` = IF(:posts, `cat_count_posts` + 1, `cat_count_posts`), `cat_last_post_id` = COALESCE(:post, `cat_last_post_id`) WHERE `cat_id` = :id');
$bump->bindValue('topics', $topics ? 1 : 0);
$bump->bindValue('posts', $posts ? 1 : 0);
$bump->bindValue('post', $post);
$bump->bindValue('id', $category);
$bump->execute();
}
function category_breadcrumbs(int $category, bool $excludeSelf): array {
global $pdo;
$breadcrumbs = [];
$getBreadcrumb = $pdo->prepare('
SELECT `cat_id`, `cat_name`, `cat_parent`
FROM `fmf_categories`
WHERE `cat_id` = :category
');
while($category > 0) {
$getBreadcrumb->bindValue('category', $category);
$breadcrumb = $getBreadcrumb->execute() ? $getBreadcrumb->fetch(PDO::FETCH_ASSOC) : [];
if(empty($breadcrumb)) {
break;
}
$breadcrumbs[] = $breadcrumb;
$category = $breadcrumb['cat_parent'];
}
if($excludeSelf)
$breadcrumbs = array_slice($breadcrumbs, 1);
return array_reverse($breadcrumbs);
}
function category_child_ids(int $category): array {
global $pdo;
if($category < 1)
return [];
static $cached = [];
if(isset($cached[$category]))
return $cached[$category];
$getChildren = $pdo->prepare('
SELECT `cat_id`
FROM `fmf_categories`
WHERE `cat_parent` = :category
');
$getChildren->bindValue('category', $category);
$children = $getChildren->fetchAll(PDO::FETCH_ASSOC);
return $cached[$category] = array_column($children, 'cat_id');
}

96
include/_csrf.php Normal file
View file

@ -0,0 +1,96 @@
<?php
// Taken from Hanyuu/id.flashii.net
class CSRF {
public const TOLERANCE = 10 * 60;
public const HASH_ALGO = 'sha256';
public const EPOCH = 1572566400;
private $timestamp = 0;
private $tolerance = 0;
private static $globalIdentity = '';
private static $globalSecretKey = '';
public function __construct(int $tolerance = self::TOLERANCE, ?int $timestamp = null) {
$this->setTolerance($tolerance);
$this->setTimestamp($timestamp ?? self::timestamp());
}
public static function timestamp(): int {
return time() - self::EPOCH;
}
public static function setGlobalIdentity(string $identity): void {
self::$globalIdentity = $identity;
}
public static function setGlobalSecretKey(string $secretKey): void {
self::$globalSecretKey = $secretKey;
}
public static function validate(string $token): bool {
try {
return self::decode($token, self::$globalIdentity, self::$globalSecretKey)->isValid();
} catch(Exception $ex) {
return false;
}
}
public static function token(): string {
return (new static)->encode(self::$globalIdentity, self::$globalSecretKey);
}
public static function html(): string {
return sprintf('<input type="hidden" name="_csrf" value="%s"/>', self::token());
}
public static function verify(): bool {
return self::validate(!empty($_REQUEST['_csrf']) && is_string($_REQUEST['_csrf']) ? $_REQUEST['_csrf'] : '');
}
public static function decode(string $token, string $identity, string $secretKey): CSRF {
$hash = substr($token, 12);
$unpacked = unpack('Vtimestamp/vtolerance', hex2bin(substr($token, 0, 12)));
if(empty($hash) || empty($unpacked['timestamp']) || empty($unpacked['tolerance']))
throw new InvalidArgumentException('Invalid token provided.');
$csrf = new static($unpacked['tolerance'], $unpacked['timestamp']);
if(!hash_equals($csrf->getHash($identity, $secretKey), $hash))
throw new InvalidArgumentException('Modified token.');
return $csrf;
}
public function encode(string $identity, string $secretKey): string {
$token = bin2hex(pack('Vv', $this->getTimestamp(), $this->getTolerance()));
$token .= $this->getHash($identity, $secretKey);
return $token;
}
public function getHash(string $identity, string $secretKey): string {
return hash_hmac(self::HASH_ALGO, "{$identity}|{$this->getTimestamp()}|{$this->getTolerance()}", $secretKey);
}
public function getTimestamp(): int {
return $this->timestamp;
}
public function setTimestamp(int $timestamp): self {
if($timestamp < 0 || $timestamp > 0xFFFFFFFF)
throw new InvalidArgumentException('Timestamp must be within the constaints of an unsigned 32-bit integer.');
$this->timestamp = $timestamp;
return $this;
}
public function getTolerance(): int {
return $this->tolerance;
}
public function setTolerance(int $tolerance): self {
if($tolerance < 0 || $tolerance > 0xFFFF)
throw new InvalidArgumentException('Tolerance must be within the constaints of an unsigned 16-bit integer.');
$this->tolerance = $tolerance;
return $this;
}
public function isValid(): bool {
$currentTime = self::timestamp();
return $currentTime >= $this->getTimestamp() && $currentTime <= $this->getTimestamp() + $this->getTolerance();
}
}

100
include/_posts.php Normal file
View file

@ -0,0 +1,100 @@
<?php
define('FMF_POST_TYPE_MESSAGE', 0);
define('FMF_POST_TYPE_RESOLVE', 1);
define('FMF_POST_TYPE_LOCKED', 2);
define('FMF_POST_TYPE_UNLOCKED', 3);
define('FMF_POST_TYPE_UNRESOLVED', 4);
define('FMF_POST_TYPE_CONFIRMED', 5);
define('FMF_POST_TYPE_UNCONFIRMED', 6);
function create_post(int $category, int $topic, int $user, string $text): int {
return create_topic_event($category, $topic, $user, FMF_POST_TYPE_MESSAGE, $text);
}
function create_topic_event(int $category, int $topic, int $user, int $type, ?string $data = null): int {
global $pdo;
$createPost = $pdo->prepare('INSERT INTO `fmf_posts` (`cat_id`, `topic_id`, `user_id`, `post_type`, `post_text`) VALUES (:category, :topic, :user, :type, :text)');
$createPost->bindValue('category', $category);
$createPost->bindValue('topic', $topic);
$createPost->bindValue('user', $user);
$createPost->bindValue('type', $type);
$createPost->bindValue('text', $data);
$createPost->execute();
return (int)$pdo->lastInsertId();
}
function posts_in_topic(int $topic): array {
global $pdo;
if($topic < 1)
return [];
$getTopics = $pdo->prepare('SELECT *, UNIX_TIMESTAMP(`post_created`) AS `post_created`, UNIX_TIMESTAMP(`post_edited`) AS `post_edited`, UNIX_TIMESTAMP(`post_deleted`) AS `post_deleted` FROM `fmf_posts` WHERE `topic_id` = :topic ORDER BY `post_created`');
$getTopics->bindValue('topic', $topic);
$topics = $getTopics->execute() ? $getTopics->fetchAll(PDO::FETCH_ASSOC) : false;
return $topics ? $topics : [];
}
function post_info(?int $post): array {
global $pdo;
static $posts = [];
if($post < 1)
return [];
if(!empty($posts[$post]))
return $posts[$post];
$getPost = $pdo->prepare('SELECT *, UNIX_TIMESTAMP(`post_created`) AS `post_created`, UNIX_TIMESTAMP(`post_edited`) AS `post_edited`, UNIX_TIMESTAMP(`post_deleted`) AS `post_deleted` FROM `fmf_posts` WHERE `post_id` = :post');
$getPost->bindValue('post', $post);
$postInfo = $getPost->execute() ? $getPost->fetch(PDO::FETCH_ASSOC) : false;
return $posts[$post] = ($postInfo ? $postInfo : []);
}
function post_delete(int $post, bool $hard = false): void {
global $pdo;
if($post < 1)
return;
$delete = $pdo->prepare($hard ? 'DELETE FROM `fmf_posts` WHERE `post_id` = :post' : 'UPDATE `fmf_posts` SET `post_deleted` = NOW() WHERE `post_id` = :post');
$delete->bindValue('post', $post);
$delete->execute();
}
function post_restore(int $post): void {
global $pdo;
if($post < 1)
return;
$restore = $pdo->prepare('UPDATE `fmf_posts` SET `post_deleted` = NULL WHERE `post_id` = :post');
$restore->bindValue('post', $post);
$restore->execute();
}
function post_anonymize(int $post): void {
global $pdo;
if($post < 1)
return;
$restore = $pdo->prepare('UPDATE `fmf_posts` SET `user_id` = NULL WHERE `post_id` = :post');
$restore->bindValue('post', $post);
$restore->execute();
}
function post_update(int $post, string $text): void {
global $pdo;
if($post < 1)
return;
$restore = $pdo->prepare('UPDATE `fmf_posts` SET `post_text` = :text, `post_edited` = NOW() WHERE `post_id` = :post');
$restore->bindValue('text', $text);
$restore->bindValue('post', $post);
$restore->execute();
}

78
include/_topics.php Normal file
View file

@ -0,0 +1,78 @@
<?php
function topics_in_category(int $category): array {
global $pdo;
if($category < 1)
return [];
$getTopics = $pdo->prepare('SELECT *, UNIX_TIMESTAMP(`topic_created`) AS `topic_created`, UNIX_TIMESTAMP(`topic_bumped`) AS `topic_bumped`, UNIX_TIMESTAMP(`topic_locked`) AS `topic_locked`, UNIX_TIMESTAMP(`topic_resolved`) AS `topic_resolved`, UNIX_TIMESTAMP(`topic_confirmed`) AS `topic_confirmed` FROM `fmf_topics` WHERE `cat_id` = :category AND `topic_bumped` IS NOT NULL ORDER BY `topic_bumped` DESC');
$getTopics->bindValue('category', $category);
$topics = $getTopics->execute() ? $getTopics->fetchAll(PDO::FETCH_ASSOC) : false;
return $topics ? $topics : [];
}
function create_topic(int $category, int $user, string $title): int {
global $pdo;
$createTopic = $pdo->prepare('INSERT INTO `fmf_topics` (`cat_id`, `user_id`, `topic_title`) VALUES (:cat, :user, :title)');
$createTopic->bindValue('cat', $category);
$createTopic->bindValue('user', $user);
$createTopic->bindValue('title', $title);
$createTopic->execute();
return (int)$pdo->lastInsertId();
}
function topic_info(int $topic): array {
global $pdo;
if($topic < 1)
return [];
$getTopic = $pdo->prepare('SELECT *, UNIX_TIMESTAMP(`topic_created`) AS `topic_created`, UNIX_TIMESTAMP(`topic_locked`) AS `topic_locked`, UNIX_TIMESTAMP(`topic_resolved`) AS `topic_resolved`, UNIX_TIMESTAMP(`topic_confirmed`) AS `topic_confirmed` FROM `fmf_topics` WHERE `topic_id` = :id');
$getTopic->bindValue('id', $topic);
$topic = $getTopic->execute() ? $getTopic->fetch(PDO::FETCH_ASSOC) : false;
return $topic ? $topic : [];
}
function topic_bump(int $topic, ?int $post = null, bool $onlyPostId = false): void {
global $pdo;
if($topic < 1)
return;
$bump = $pdo->prepare('UPDATE `fmf_topics` SET `topic_bumped` = IF(:no_bump, `topic_bumped`, NOW()), `topic_count_replies` = IF(`topic_bumped` IS NULL, `topic_count_replies`, `topic_count_replies` + 1), `topic_last_post_id` = COALESCE(:post, `topic_last_post_id`) WHERE `topic_id` = :topic');
$bump->bindValue('topic', $topic);
$bump->bindValue('post', $post);
$bump->bindValue('no_bump', $onlyPostId ? 1 : 0);
$bump->execute();
}
function lock_topic(int $topic, bool $state): void {
global $pdo;
$lock = $pdo->prepare('UPDATE `fmf_topics` SET `topic_locked` = IF(:state, NOW(), NULL) WHERE `topic_id` = :topic');
$lock->bindValue('state', $state ? 1 : 0);
$lock->bindValue('topic', $topic);
$lock->execute();
}
function mark_topic_resolved(int $topic, bool $state): void {
global $pdo;
$resolve = $pdo->prepare('UPDATE `fmf_topics` SET `topic_resolved` = IF(:state, NOW(), NULL) WHERE `topic_id` = :topic');
$resolve->bindValue('state', $state ? 1 : 0);
$resolve->bindValue('topic', $topic);
$resolve->execute();
}
function mark_topic_confirmed(int $topic, bool $state): void {
global $pdo;
$confirm = $pdo->prepare('UPDATE `fmf_topics` SET `topic_confirmed` = IF(:state, NOW(), NULL) WHERE `topic_id` = :topic');
$confirm->bindValue('state', $state ? 1 : 0);
$confirm->bindValue('topic', $topic);
$confirm->execute();
}

94
include/_track.php Normal file
View file

@ -0,0 +1,94 @@
<?php
include_once '_category.php';
function track_check_category(int $user, int $category): int {
global $pdo;
static $cache = [];
if($user < 1 || $category < 1)
return false;
$trackId = "{$user}-{$category}";
if(isset($cache[$trackId]))
return $cache[$trackId];
$cache[$trackId] = 0;
$children = category_child_ids($category);
foreach($children as $child)
$cache[$trackId] += track_check_category($user, $child);
$countUnread = $pdo->prepare('
SELECT COUNT(ti.`topic_id`)
FROM `fmf_topics` AS ti
LEFT JOIN `fmf_track` AS tt
ON tt.`topic_id` = ti.`topic_id` AND tt.`user_id` = :user
WHERE ti.`cat_id` = :category
AND ti.`topic_bumped` >= NOW() - INTERVAL 1 MONTH
AND (
tt.`track_timestamp` IS NULL
OR tt.`track_timestamp` < ti.`topic_bumped`
)
');
$countUnread->bindValue('category', $category);
$countUnread->bindValue('user', $user);
$cache[$trackId] += $countUnread->execute() ? (int)$countUnread->fetchColumn() : 0;
return $cache[$trackId];
}
function track_check_topic(int $user, int $topic): bool {
global $pdo;
static $cache = [];
if($user < 1 || $topic < 1)
return false;
$trackId = "{$user}-{$topic}";
if(isset($cache[$trackId]))
return $cache[$trackId];
$getUnread = $pdo->prepare('
SELECT
:user AS `target_user_id`,
(
SELECT
`target_user_id` > 0
AND
t.`topic_bumped` > NOW() - INTERVAL 1 MONTH
AND (
SELECT COUNT(ti.`topic_id`) < 1
FROM `fmf_track` AS tt
RIGHT JOIN `fmf_topics` AS ti
ON ti.`topic_id` = tt.`topic_id`
WHERE ti.`topic_id` = t.`topic_id`
AND tt.`user_id` = `target_user_id`
AND `track_timestamp` >= `topic_bumped`
)
)
FROM `fmf_topics` AS t
WHERE t.`topic_id` = :topic
');
$getUnread->bindValue('user', $user);
$getUnread->bindValue('topic', $topic);
return $cache[$trackId] = ($getUnread->execute() ? $getUnread->fetchColumn(1) : false);
}
function update_track(int $user, int $topic, int $category): void {
global $pdo;
if($user < 1 || $topic < 1 || $category < 1)
return;
$updateTrack = $pdo->prepare('
REPLACE INTO `fmf_track`
(`cat_id`, `topic_id`, `user_id`)
VALUES
(:category, :topic, :user)
');
$updateTrack->bindValue('category', $category);
$updateTrack->bindValue('topic', $topic);
$updateTrack->bindValue('user', $user);
$updateTrack->execute();
}

239
include/_user.php Normal file
View file

@ -0,0 +1,239 @@
<?php
include_once '_utils.php';
define('FMF_UF_SCROLLBEYOND', 1);
function get_user_id(string $username, string $email): int {
global $pdo;
$checkUser = $pdo->prepare('SELECT `user_id` FROM `fmf_users` WHERE LOWER(`user_login`) = LOWER(:login) OR LOWER(`user_email`) = LOWER(:email)');
$checkUser->bindValue('login', $username);
$checkUser->bindValue('email', $email);
$checkUser->execute();
return (int)$checkUser->fetchColumn();
}
function get_user_for_login(string $nameOrMail): array {
global $pdo;
$getUser = $pdo->prepare('SELECT `user_id`, `user_login`, `user_password`, `user_email_verification` FROM `fmf_users` WHERE LOWER(`user_login`) = LOWER(:login) OR LOWER(`user_email`) = LOWER(:email)');
$getUser->bindValue('login', $nameOrMail);
$getUser->bindValue('email', $nameOrMail);
$user = $getUser->execute() ? $getUser->fetch(PDO::FETCH_ASSOC) : false;
return $user ? $user : [];
}
function user_info(?int $user, bool $fresh = false): array {
global $pdo;
static $cache = [];
if($user < 1)
return [];
if(!$fresh && !empty($cache[$user]))
return $cache[$user];
$getUserInfo = $pdo->prepare('SELECT *, UNIX_TIMESTAMP(`user_created`) AS `user_created`, UNIX_TIMESTAMP(`user_banned`) AS `user_banned`, MD5(LOWER(TRIM(`user_email`))) AS `gravatar_hash` FROM `fmf_users` WHERE `user_id` = :user');
$getUserInfo->bindValue('user', $user);
$userInfo = $getUserInfo->execute() ? $getUserInfo->fetch(PDO::FETCH_ASSOC) : false;
return $cache[$user] = ($userInfo ? $userInfo : []);
}
function user_has_flag(int $user, int $flag, bool $strict = false): bool {
$userInfo = user_info($user);
if(empty($userInfo))
return false;
$flags = ($userInfo['user_flags'] & $flag);
return $strict ? ($flags === $flag) : ($flags > 0);
}
function create_user(string $username, string $email, string $password, string $ipAddr, bool $verified = false): array {
global $pdo;
$verification = $verified ? null : bin2hex(random_bytes(16));
$createUser = $pdo->prepare('INSERT INTO `fmf_users` (`user_login`, `user_password`, `user_email`, `user_email_verification`, `user_ip_created`) VALUES (:login, :password, :email, :verification, INET6_ATON(:ip))');
$createUser->bindValue('login', $username);
$createUser->bindValue('password', password_hash($password, PASSWORD_DEFAULT));
$createUser->bindValue('email', $email);
$createUser->bindValue('verification', $verification);
$createUser->bindValue('ip', $ipAddr);
$createUser->execute();
return [
'user_id' => (int)$pdo->lastInsertId(),
'verification' => $verification,
];
}
function validate_username(string $username): ?string {
if($username !== trim($username))
return 'Your username may not start or end with spaces.';
$usernameLength = strlen($username);
if($usernameLength < 3)
return 'Your username must be longer than 3 characters.';
if($usernameLength > 16)
return 'Your username may not be longer than 16 characters.';
if(!preg_match('#^[A-Za-z0-9-_]+$#u', $username))
return 'Your username may only contains alphanumeric characters, dashes and underscores (A-Z, a-z, 0-9, -, _).';
return null;
}
function validate_email(string $email): ?string {
if(filter_var($email, FILTER_VALIDATE_EMAIL) === false)
return 'Your e-mail address is not correctly formatted.';
$domain = mb_substr(mb_strstr($email, '@'), 1);
if(!checkdnsrr($domain, 'MX') && !checkdnsrr($domain, 'A'))
return 'Your e-mail address domain does not have valid DNS records.';
return null;
}
function validate_password(string $password): ?string {
if(unique_chars($password) < 10)
return 'Your password is too weak.';
return null;
}
function activate_user(string $code): void {
global $pdo;
if(strlen($code) !== 32)
return;
$verify = $pdo->prepare('UPDATE `fmf_users` SET `user_email_verification` = NULL WHERE `user_email_verification` = :code');
$verify->bindValue('code', $code);
$verify->execute();
}
function create_session(int $userId): string {
global $pdo;
if($userId < 1)
return '';
$sessionKey = bin2hex(random_bytes(32));
$createSession = $pdo->prepare('INSERT INTO `fmf_sessions` (`user_id`, `sess_key`) VALUES (:user, :key)');
$createSession->bindValue('user', $userId);
$createSession->bindValue('key', $sessionKey);
$createSession->execute();
return $sessionKey;
}
function purge_old_sessions(): void {
global $pdo;
$pdo->exec('DELETE FROM `fmf_sessions` WHERE `sess_created` + INTERVAL 1 MONTH <= NOW()');
}
function session_activate(?string $key): void {
global $pdo;
if(empty($key) || strlen($key) !== 64)
return;
$verify = $pdo->prepare('SELECT `user_id` FROM `fmf_sessions` WHERE `sess_key` = :key AND `sess_created` + INTERVAL 1 MONTH > NOW()');
$verify->bindValue('key', $key);
$userId = $verify->execute() ? $verify->fetchColumn() : 0;
if($userId < 1)
return;
$GLOBALS['fmf_user_id'] = (int)$userId;
}
function session_active(): bool {
return !empty($GLOBALS['fmf_user_id']) && is_int($GLOBALS['fmf_user_id']) && $GLOBALS['fmf_user_id'] > 0;
}
function logout_token(): string {
$sessionKey = $_COOKIE['fmfauth'] ?? '';
if(strlen($sessionKey) !== 64 || !ctype_xdigit($sessionKey))
return bin2hex(random_bytes(4));
$offset = hexdec($sessionKey[0]) * 2;
$offset = hexdec($sessionKey[$offset]) * 2;
$offset = hexdec($sessionKey[$offset]) * 2;
return substr($sessionKey, $offset, 8);
}
function destroy_session(string $token): void {
global $pdo;
$delete = $pdo->prepare('DELETE FROM `fmf_sessions` WHERE `sess_key` = :key');
$delete->bindValue('key', $token);
$delete->execute();
}
function current_user_id(): int {
return session_active() ? $GLOBALS['fmf_user_id'] : 0;
}
function verify_password(string $pass, ?int $user = null): bool {
global $pdo;
$user = $user ?? current_user_id();
if($user < 1)
return false;
$getHash = $pdo->prepare('SELECT `user_password` FROM `fmf_users` WHERE `user_id` = :user');
$getHash->bindValue('user', $user);
$hash = $getHash->execute() ? $getHash->fetchColumn() : '';
if(empty($hash))
return false;
return password_verify($pass, $hash);
}
function user_set_password(int $user, string $password): void {
global $pdo;
if($user < 1)
return;
$password = password_hash($password, PASSWORD_DEFAULT);
$update = $pdo->prepare('UPDATE `fmf_users` SET `user_password` = :pass WHERE `user_id` = :user');
$update->bindValue('pass', $password);
$update->bindValue('user', $user);
$update->execute();
}
function user_set_email(int $user, string $email, bool $verified = false): ?string {
global $pdo;
if($user < 1)
return null;
$verification = $verified ? null : bin2hex(random_bytes(16));
$update = $pdo->prepare('UPDATE `fmf_users` SET `user_email` = LOWER(:mail), `user_email_verification` = :verf WHERE `user_id` = :user');
$update->bindValue('mail', $email);
$update->bindValue('verf', $verification);
$update->bindValue('user', $user);
$update->execute();
return $verification;
}
function user_gravatar(?int $user, int $res = 80): string {
$authorInfo = user_info($user);
return '//www.gravatar.com/avatar/'. ($authorInfo['gravatar_hash'] ?? str_repeat('0', 32)) .'?s='. $res .'&amp;r=g&amp;d=identicon';
}

17
include/_utils.php Normal file
View file

@ -0,0 +1,17 @@
<?php
function unique_chars(string $input, bool $multibyte = true): int {
$chars = [];
$strlen = $multibyte ? 'mb_strlen' : 'strlen';
$substr = $multibyte ? 'mb_substr' : 'substr';
$length = $strlen($input);
for($i = 0; $i < $length; $i++) {
$current = $substr($input, $i, 1);
if(!in_array($current, $chars, true)) {
$chars[] = $current;
}
}
return count($chars);
}

10
layout/banned.php Normal file
View file

@ -0,0 +1,10 @@
<?php
$message = 'You were banned on ' . date(FMF_DATE_FORMAT, $banTimestamp) . '.<br/>';
if(empty($banReason)) {
$message .= '<i>No reason was provided.</i>';
} else {
$message .= $banReason;
}
include_once __DIR__ . '/notice.php';

15
layout/footer.php Normal file
View file

@ -0,0 +1,15 @@
<div class="footer">
Powered by Chie<br/>
&copy; <a href="https://flash.moe">Flashwave</a> 2019-<?=date('Y');?>
<?php
if(!empty($extendedFooter)) {
?>
<br/>
Theme inspired by <a href="https://www.phpbb.com/customise/db/style/darksky/">darksky</a> for phpBB by Skysect
<?php
}
?>
</div>
</div>
</body>
</html>

48
layout/header.php Normal file
View file

@ -0,0 +1,48 @@
<?php
include_once '_user.php';
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title><?=$title ?? 'flash.moe message board';?></title>
<link href="/style.css" type="text/css" rel="stylesheet"/>
<script type="text/javascript">
var _paq = window._paq || [];
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
_paq.push(['setTrackerUrl', '//uiharu.railgun.sh/mtm']);
_paq.push(['setSiteId', 'w4PqjBGmOL5l']);
var g = document.createElement('script');
g.type = 'text/javascript'; g.async = true;
g.defer = true; g.src = '//uiharu.railgun.sh/mtm.js';
document.head.appendChild(g);
})();
</script>
</head>
<body>
<div class="wrapper<?php if(user_has_flag(current_user_id(), FMF_UF_SCROLLBEYOND)) { echo ' scrollbeyond'; }?>">
<div class="header">
<h1>flash.moe message board</h1>
<div class="header-wrap">
<div class="header-nav">
<a href="/">Home</a>
<?php if(session_active()) { ?>
<a href="/settings">Settings</a>
<a href="/logout/<?=logout_token();?>">Log out</a>
<?php } else { ?>
<a href="/login">Log in</a>
<a href="/register">Register</a>
<?php } ?>
</div>
<?php if(empty($hideSearch)) { ?>
<form method="get" action="/search" class="header-search">
<input type="search" name="q"/>
<input type="submit" value="Search"/>
</form>
<?php } ?>
</div>
</div>

6
layout/notice.php Normal file
View file

@ -0,0 +1,6 @@
<?php
include_once FMF_LAYOUT . '/header.php';
echo $message ?? '';
include_once FMF_LAYOUT . '/footer.php';

4
public/404.php Normal file
View file

@ -0,0 +1,4 @@
<?php
require_once '../startup.php';
die_ex('Page not found.', 404);

9
public/activate.php Normal file
View file

@ -0,0 +1,9 @@
<?php
require_once '../startup.php';
include_once '_user.php';
if(isset($_GET['key']) && is_string($_GET['key']))
activate_user($_GET['key']);
header('Location: /login?m=activated');

132
public/category.php Normal file
View file

@ -0,0 +1,132 @@
<?php
require_once '../startup.php';
include_once '_category.php';
include_once '_topics.php';
include_once '_posts.php';
include_once '_user.php';
include_once '_track.php';
$catId = isset($_GET['id']) && is_string($_GET['id']) && ctype_digit($_GET['id']) ? (int)$_GET['id'] : 0;
$categoryInfo = category_info($catId);
if(!$categoryInfo) {
die_ex('Invalid category', 404);
}
if($categoryInfo['cat_type'] == 2) {
http_response_code(302);
header('Location: ' . $categoryInfo['cat_link']);
return;
}
$title = $categoryInfo['cat_name'];
include FMF_LAYOUT . '/header.php';
$breadcrumbs_arr = category_breadcrumbs($categoryInfo['cat_id'], true);
$breadcrumbs = '<a href="/">forum.flash.moe</a> &raquo; ';
foreach($breadcrumbs_arr as $breadcrumb) {
$breadcrumbs .= sprintf('<a href="/category/%d">%s</a> &raquo; ', $breadcrumb['cat_id'], $breadcrumb['cat_name']);
}
echo $breadcrumbs;
?>
<h3 class="forum-title"><?=$categoryInfo['cat_name'];?></h3>
<?php
$categories = category_children($categoryInfo['cat_id'], 2);
if(count($categories) > 0) {
?>
<div class="forum-category">
<div class="forum-category-title">
<div class="forum-category-title-info">Categories</div>
<div class="forum-category-count">Topics</div>
<div class="forum-category-count">Posts</div>
<div class="forum-category-latest forum-category-latest-header">Latest post</div>
</div>
<div class="forum-category-children">
<?php
foreach($categories as $cat1) {
$trackStatus = track_check_category(current_user_id(), $cat1['cat_id']);
?>
<div class="forum-category-board">
<div class="forum-category-board-indicator<?=($trackStatus ? ' unread' : '');?>" title="<?=($trackStatus ? 'There are unread posts' : 'No unread posts');?>"></div>
<div class="forum-category-board-info">
<a href="/category/<?=$cat1['cat_id'];?>"><?=htmlentities($cat1['cat_name']);?></a>
<div class="forum-category-board-desc"><?=htmlentities($cat1['cat_description']);?></div>
</div>
<?php if($cat1['cat_type'] != 2) { ?>
<div class="forum-category-count"><?=number_format($cat1['cat_count_topics']);?></div>
<div class="forum-category-count"><?=number_format($cat1['cat_count_posts']);?></div>
<div class="forum-category-latest">
<?php if($cat1['cat_last_post_id'] < 1) { ?>
No posts
<?php } else { $postInfo = post_info($cat1['cat_last_post_id']); ?>
<a href="/post/<?=$cat1['cat_last_post_id'];?>">#<?=$cat1['cat_last_post_id'];?></a><br/>
<time datetime="<?=date('c', $postInfo['post_created']);?>"><?=date(FMF_DATE_FORMAT, $postInfo['post_created']);?></time>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
</div>
<?php
}
if($categoryInfo['cat_type'] == 0) {
$topics = topics_in_category($categoryInfo['cat_id']);
?>
<a href="/category/<?=$categoryInfo['cat_id'];?>/create" class="createtopicbtn">Create Topic</a>
<div class="topics">
<div class="topics-header">
<div class="topics-header-info">Topics</div>
<div class="topics-item-author">Author</div>
<div class="topics-item-created">Created</div>
<div class="topics-item-count">Posts</div>
<div class="topics-item-latest topics-item-latest-header">Latest reply</div>
</div>
<div class="topics-items">
<?php
foreach($topics as $topic) {
$authorInfo = user_info($topic['user_id']);
$trackStatus = track_check_topic(current_user_id(), $topic['topic_id']);
?>
<div class="topics-item">
<div class="topics-item-indicator<?=($trackStatus ? ' unread' : '');?>" title="<?=($trackStatus ? 'There are unread posts' : 'No unread posts');?>">
</div>
<?php if(!empty($topic['topic_resolved'])) { ?>
<img src="/images/tick.png" title="<?=($categoryInfo['cat_variation'] === 1 ? 'Implemented' : 'Resolved');?>" class="topics-item-status" alt="<?=($categoryInfo['cat_variation'] === 1 ? 'Implemented' : 'Resolved');?>" class="topics-item-status"/>
<?php } elseif(!empty($topic['topic_confirmed'])) { ?>
<img src="/images/<?=($categoryInfo['cat_variation'] === 1 ? 'star' : 'error');?>.png" title="<?=($categoryInfo['cat_variation'] === 1 ? 'Accepted' : 'Confirmed');?>" alt="<?=($categoryInfo['cat_variation'] === 1 ? 'Accepted' : 'Confirmed');?>" class="topics-item-status"/>
<?php } ?>
<?php if(!empty($topic['topic_locked'])) { ?>
<img src="/images/lock.png" title="Locked" alt="Locked" class="topics-item-status"/>
<?php } ?>
<div class="topics-item-info">
<a href="/topic/<?=$topic['topic_id'];?>"><?=htmlentities($topic['topic_title']);?></a>
</div>
<div class="topics-item-author"><a href="/user/<?=$authorInfo['user_id'] ?? 0;?>"><?=$authorInfo['user_login'] ?? 'Deleted User';?></a></div>
<div class="topics-item-created">
<time datetime="<?=date('c', $topic['topic_created']);?>"><?=date(FMF_DATE_FORMAT, $topic['topic_created']);?></time>
</div>
<div class="topics-item-count"><?=number_format($topic['topic_count_replies']);?></div>
<div class="topics-item-latest">
<?php if($topic['topic_last_post_id'] < 1) { ?>
No replies
<?php } else { $postInfo = post_info($topic['topic_last_post_id']); ?>
<a href="/post/<?=$postInfo['post_id'];?>">#<?=$postInfo['post_id'];?></a><br/>
<time datetime="<?=date('c', $postInfo['post_created']);?>"><?=date(FMF_DATE_FORMAT, $postInfo['post_created']);?></time>
<?php } ?>
</div>
</div>
<?php
}
?>
</div>
</div>
<a href="/category/<?=$categoryInfo['cat_id'];?>/create" class="createtopicbtn">Create Topic</a>
<?php
}
include FMF_LAYOUT . '/footer.php';

26
public/hook/github.php Normal file
View file

@ -0,0 +1,26 @@
<?php
require_once '../../startup.php';
header('Content-Type: text/plain; charset=utf-8');
function die_gh(int $code, string $msg = ''): void {
http_response_code($code);
echo $msg;
exit;
}
if(!defined('GITHUB_SECRET') || empty(GITHUB_SECRET))
die_gh(500, 'no token defined');
$rawBody = file_get_contents('php://input');
if(empty($rawBody))
die_gh(404, 'no data');
$sig = explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE'], 2);
if(count($sig) !== 2 || $sig[0] !== 'sha1' || !hash_equals(hash_hmac($sig[0], $rawBody, GITHUB_SECRET), $sig[1]))
die_gh(403, 'invalid signature');
$body = json_decode($_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded' ? $_POST['payload'] : $rawBody);

BIN
public/images/accept.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

BIN
public/images/bomb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

BIN
public/images/cross.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

BIN
public/images/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

BIN
public/images/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

BIN
public/images/lock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

BIN
public/images/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

BIN
public/images/thumb_up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

BIN
public/images/tick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

54
public/index.php Normal file
View file

@ -0,0 +1,54 @@
<?php
require_once '../startup.php';
include_once '_category.php';
include_once '_track.php';
include_once '_posts.php';
include FMF_LAYOUT . '/header.php';
$categories = root_category();
foreach($categories as $cat1) {
?>
<div class="forum-category">
<div class="forum-category-title">
<div class="forum-category-title-info">
<a href="/category/<?=$cat1['cat_id'];?>"><?=$cat1['cat_name'];?></a>
</div>
<div class="forum-category-count">Topics</div>
<div class="forum-category-count">Posts</div>
<div class="forum-category-latest forum-category-latest-header">Latest post</div>
</div>
<div class="forum-category-children">
<?php
foreach($cat1['children'] as $cat2) {
$trackStatus = track_check_category(current_user_id(), $cat2['cat_id']);
?>
<div class="forum-category-board">
<div class="forum-category-board-indicator<?=($trackStatus ? ' unread' : '');?>" title="<?=($trackStatus ? 'There are unread posts' : 'No unread posts');?>"></div>
<div class="forum-category-board-info">
<a href="/category/<?=$cat2['cat_id'];?>"><?=htmlentities($cat2['cat_name']);?></a>
<div class="forum-category-board-desc"><?=htmlentities($cat2['cat_description']);?></div>
</div>
<?php if($cat2['cat_type'] != 2) { ?>
<div class="forum-category-count"><?=number_format($cat2['cat_count_topics']);?></div>
<div class="forum-category-count"><?=number_format($cat2['cat_count_posts']);?></div>
<div class="forum-category-latest">
<?php if($cat2['cat_last_post_id'] < 1) { ?>
No posts
<?php } else { $postInfo = post_info($cat2['cat_last_post_id']); ?>
<a href="/post/<?=$cat2['cat_last_post_id'];?>">#<?=$cat2['cat_last_post_id'];?></a><br/>
<time datetime="<?=date('c', $postInfo['post_created']);?>"><?=date(FMF_DATE_FORMAT, $postInfo['post_created']);?></time>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
</div>
<?php
}
$extendedFooter = true;
include FMF_LAYOUT . '/footer.php';

71
public/login.php Normal file
View file

@ -0,0 +1,71 @@
<?php
require_once '../startup.php';
include_once '_user.php';
if(session_active()) {
header('Location: /');
return;
}
if(isset($_POST['username'], $_POST['password']) && CSRF::verify()) {
$username = is_string($_POST['username']) ? $_POST['username'] : '';
$password = is_string($_POST['password']) ? $_POST['password'] : '';
$userInfo = get_user_for_login($username);
if(empty($userInfo) || !password_verify($password, $userInfo['user_password'])) {
$error = 'Username or password was invalid.';
} elseif(!empty($userInfo['user_email_verification'])) {
$error = 'You must complete e-mail verification before logging in.';
} else {
$sessionKey = create_session($userInfo['user_id']);
if(empty($sessionKey)) {
$error = 'Failed to start a session.';
} else {
setcookie('fmfauth', $sessionKey, time() + (60 * 60 * 24 * 31), '/');
header('Location: /');
return;
}
}
} else {
switch(!empty($_GET['m']) && is_string($_GET['m']) ? $_GET['m'] : '') {
case 'welcome':
$message = 'You account has been created.';
break;
case 'activated':
$message = 'Your account has been activated.';
break;
case 'reactivate':
$message = 'You must reactivate your account after changing your e-mail address.';
break;
case 'forbidden':
$error = 'You must be logged in to do that.';
break;
}
}
include FMF_LAYOUT . '/header.php';
?>
<form class="auth-form" method="post" action="">
<?=CSRF::html();?>
<div class="auth-header">
<h1>Log in</h1>
</div>
<?php if(isset($error) || isset($message)) { ?>
<div class="auth-message<?php if(isset($error)) { echo ' auth-message-error'; }?>"><?=($error ?? $message);?></div>
<?php } ?>
<label class="auth-field">
<div class="auth-field-name">Username</div>
<div class="auth-field-value"><input type="text" name="username" value="<?=htmlentities($username ?? '');?>"/></div>
</label>
<label class="auth-field">
<div class="auth-field-name">Password</div>
<div class="auth-field-value"><input type="password" name="password"/></div>
</label>
<div class="auth-buttons">
<input type="submit" value="Log in"/>
</div>
</form>
<?php
include FMF_LAYOUT . '/footer.php';

11
public/logout.php Normal file
View file

@ -0,0 +1,11 @@
<?php
require_once '../startup.php';
include_once '_user.php';
$logoutToken = !empty($_GET['key']) && is_string($_GET['key']) ? $_GET['key'] : '';
if(session_active() && $logoutToken === logout_token())
destroy_session($_COOKIE['fmfauth'] ?? '');
header('Location: /');

35
public/post.php Normal file
View file

@ -0,0 +1,35 @@
<?php
require_once '../startup.php';
include_once '_posts.php';
$postId = isset($_GET['id']) && is_string($_GET['id']) && ctype_digit($_GET['id']) ? (int)$_GET['id'] : 0;
$mode = isset($_GET['m']) && is_string($_GET['m']) ? $_GET['m'] : '';
$postInfo = post_info($postId);
$userInfo = user_info(current_user_id());
$userActive = !empty($userInfo);
if(empty($postInfo))
die_ex('Post not found.', 404);
switch($mode) {
case 'delete':
if(!CSRF::verify() || !$userActive || !($userInfo['user_moderator'] || $userInfo['user_id'] === $postInfo['user_id']))
die_ex('You can\'t delete this post.', 403);
post_delete($postInfo['post_id']);
break;
case 'restore':
if(!CSRF::verify() || !$userActive || !$userInfo['user_moderator'])
die_ex('You can\'t restore this post.', 403);
post_restore($postInfo['post_id']);
break;
case 'anonymize':
if(!CSRF::verify() || !$userActive || !$userInfo['user_moderator'])
die_ex('You can\'t strip the user id of this post.', 403);
post_anonymize($postInfo['post_id']);
break;
}
header("Location: /topic/{$postInfo['topic_id']}#p{$postInfo['post_id']}");

151
public/posting.php Normal file
View file

@ -0,0 +1,151 @@
<?php
require_once '../startup.php';
include_once '_category.php';
include_once '_user.php';
include_once '_topics.php';
include_once '_posts.php';
if(!session_active()) {
header('Location: /login?m=forbidden');
return;
}
$userInfo = user_info(current_user_id());
$categoryId = isset($_GET['cat']) && is_string($_GET['cat']) && ctype_digit($_GET['cat']) ? (int)$_GET['cat'] : 0;
$topicId = isset($_GET['topic']) && is_string($_GET['topic']) && ctype_digit($_GET['topic']) ? (int)$_GET['topic'] : 0;
$postId = isset($_GET['post']) && is_string($_GET['post']) && ctype_digit($_GET['post']) ? (int)$_GET['post'] : 0;
if($postId > 0) {
$postInfo = post_info($postId);
if(empty($postInfo))
die_ex('Post not found.', 404);
if($postInfo['post_type'] != FMF_POST_TYPE_MESSAGE)
die_ex('This is not a message.', 400);
if(!$userInfo['user_moderator'] && $userInfo['user_id'] != $postInfo['user_id'])
die_ex('You aren\'t allowed to edit this post.', 403);
$categoryId = $postInfo['cat_id'] ?? 0;
$topicId = $postInfo['topic_id'] ?? 0;
$postId = $postInfo['post_id'] ?? 0;
$postText = $postInfo['post_text'] ?? '';
} else {
$postId = 0;
}
if($topicId > 0) {
$topicInfo = topic_info($topicId);
if(empty($topicInfo))
die_ex('Topic not found.', 404);
$categoryId = $topicInfo['cat_id'] ?? 0;
$topicId = $topicInfo['topic_id'] ?? 0;
} else {
$topicId = 0;
}
$categoryInfo = category_info($categoryId);
if(empty($categoryInfo)) {
die_ex('Category does not exist.', 404);
}
if($categoryInfo['cat_type'] != 0) {
die_ex('This category cannot hold topics.');
}
if(isset($topicInfo)) {
if(!empty($topicInfo['topic_locked']) && !$userInfo['user_moderator']) {
die_ex('You may not respond to locked topics.', 403);
}
}
$title = isset($topicInfo) ? ((isset($postInfo) ? 'Editing reply to ' : 'Replying to ') . $topicInfo['topic_title']) : ('Creating a topic in ' . $categoryInfo['cat_name']);
if(isset($_POST['text']) && CSRF::verify()) {
$postTitle = isset($_POST['title']) && is_string($_POST['title']) ? $_POST['title'] : '';
$postText = trim(is_string($_POST['text']) ? $_POST['text'] : '');
$postLen = mb_strlen($postText);
if($postLen < 10) {
$error = 'Post content must be longer than 10 characters.';
} elseif($postLen > 50000) {
$error = 'Post content may not be longer than 50000 characters.';
} else {
if(!isset($topicInfo)) {
$titleLen = mb_strlen($postTitle);
if($titleLen < 5) {
$error = 'Topic titles must be longer than 5 characters.';
} elseif($titleLen > 100) {
$error = 'Topic titles may not be longer than 100 characters.';
} else {
$topicId = create_topic($categoryInfo['cat_id'], current_user_id(), $postTitle);
if($topicId < 1) {
$error = 'Failed to create topic.';
} else {
$topicInfo = topic_info($topicId);
$satoriMsg = "[b]forum.flash.moe[/b]: [url=https://forum.flash.moe/user/{$userInfo['user_id']}][b]{$userInfo['user_login']}[/b][/url] created topic [url=https://forum.flash.moe/topic/{$topicId}][b]{$topicInfo['topic_title']}[/b][/url]";
}
}
}
if(!isset($error) && !isset($message)) {
if(isset($postInfo)) {
post_update($postInfo['post_id'], $postText);
} else {
$postId = create_post($categoryInfo['cat_id'], $topicInfo['topic_id'], current_user_id(), $postText);
topic_bump($topicInfo['topic_id'], $postId, !empty($topicInfo['topic_resolved']));
category_bump($categoryInfo['cat_id'], $postId, isset($titleLen));
if(!isset($satoriMsg))
$satoriMsg = "[b]forum.flash.moe[/b]: [url=https://forum.flash.moe/user/{$userInfo['user_id']}][b]{$userInfo['user_login']}[/b][/url] replied to [url=https://forum.flash.moe/post/{$postId}][b]{$topicInfo['topic_title']}[/b][/url]";
}
if(defined('SATORI_SECRET') && !empty($satoriMsg)) {
$sock = @fsockopen(SATORI_HOST, SATORI_PORT, $errno, $errstr, 2);
if($sock) {
fwrite($sock, chr(0xF) . hash_hmac('sha256', $satoriMsg, SATORI_SECRET) . $satoriMsg . chr(0xF));
fflush($sock);
fclose($sock);
}
}
$postUrl = isset($titleLen) ? "/topic/{$topicInfo['topic_id']}" : "/post/{$postId}";
header("Location: {$postUrl}");
return;
}
}
}
include FMF_LAYOUT . '/header.php';
$breadcrumbs = category_breadcrumbs($categoryInfo['cat_id'], empty($topicInfo));
echo '<a href="/">forum.flash.moe</a> &raquo; ';
foreach($breadcrumbs as $breadcrumb)
printf('<a href="/category/%d">%s</a> &raquo; ', $breadcrumb['cat_id'], $breadcrumb['cat_name']);
echo '<h3><a href="' . (empty($topicInfo) ? ('/category/' . $categoryInfo['cat_id']) : ('/topic/' . $topicInfo['topic_id'])) . '">' . ($topicInfo['topic_title'] ?? $categoryInfo['cat_name']) . '</a></h3>';
?>
<form class="posting-form" method="post" action="">
<?=CSRF::html();?>
<?php if(isset($error) || isset($message)) { ?>
<div class="posting-message<?php if(isset($error)) { echo ' posting-message-error'; }?>"><?=($error ?? $message);?></div>
<?php } ?>
<div class="posting-header">
<input type="text" <?php if(empty($topicInfo)) { ?>value="<?=htmlentities($postTitle ?? '');?>" name="title" class="posting-title" tabindex="1"<?php } else { ?>value="Re: <?=$topicInfo['topic_title'];?>" class="posting-title posting-title-disabled" disabled readonly<?php } ?>/>
<input type="submit" value="<?=(empty($postInfo) ? (empty($topicInfo) ? 'Post' : 'Reply') : 'Edit');?>" class="posting-submit" tabindex="3"/>
</div>
<textarea name="text" class="posting-text" tabindex="2"><?=htmlentities($postText ?? '');?></textarea>
<a href="https://guides.github.com/features/mastering-markdown/" style="font-size: .9em;" target="_blank" rel="noopener">Markdown supported</a>
</form>
<?php
include FMF_LAYOUT . '/footer.php';

114
public/register.php Normal file
View file

@ -0,0 +1,114 @@
<?php
require_once '../startup.php';
include_once '_user.php';
if(session_active()) {
header('Location: /');
return;
}
$antiSpam = $_COOKIE['fmfas'] ?? '';
if(!empty($antiSpam)) {
if(strlen($antiSpam) !== 80) {
unset($antiSpam);
} else {
$antiSpamRand = substr($antiSpam, 0, 16);
$antiSpamHash = substr($antiSpam, 16, 64);
$antiSpamHashConf = hash_hmac('sha256', $antiSpamRand, ANTI_SPAM_KEY);
if(!hash_equals($antiSpamHashConf, $antiSpamHash)) {
unset($antiSpam);
}
}
}
if(empty($antiSpam)) {
$antiSpam = bin2hex(random_bytes(8));
$antiSpam .= hash_hmac('sha256', $antiSpam, ANTI_SPAM_KEY);
setcookie('fmfas', $antiSpam, time() + 900, '/');
}
if(isset($_POST['username'], $_POST['password'], $_POST['password_confirm'], $_POST['email']) && CSRF::verify()) {
$antiSpamValue = isset($_POST[$antiSpam]) && is_string($_POST[$antiSpam]) ? $_POST[$antiSpam] : '';
if($antiSpamValue !== ANTI_SPAM_ANSWER) {
$error = 'Please check the value of the last form again.';
} else {
$username = is_string($_POST['username']) ? $_POST['username'] : '';
$password = is_string($_POST['password']) ? $_POST['password'] : '';
$email = is_string($_POST['email']) ? $_POST['email'] : '';
$error = validate_username($username) ?? validate_email($email) ?? validate_password($password);
if($error === null) {
if($password !== $_POST['password_confirm']) {
$error = 'Your passwords don\'t match.';
} elseif(get_user_id($username, $email) > 0) {
$error = 'This username or e-mail address has already been used.';
} else {
$registerInfo = create_user($username, $email, $password, $_SERVER['REMOTE_ADDR']);
if($registerInfo['user_id'] < 1) {
$error = 'Failed to create user.';
} else {
if(!empty($registerInfo['verification'])) {
setcookie('fmfas', '', 0, '/');
$mailer->send(
(new Swift_Message('flash.moe message board activation'))
->setFrom(['system@flash.moe' => 'flash.moe'])
->setTo([$email => $username])
->setBody(
"Hey {$username},\r\n\r\n".
"Click the following link to activate your account:\r\n\r\n".
"<https://{$_SERVER['HTTP_HOST']}/activate/{$registerInfo['verification']}>\r\n"
)
);
$message = 'Your account has been created! A verification link has been sent to your e-mail address.';
} else {
header('Location: /login?m=welcome');
return;
}
}
}
}
}
}
include FMF_LAYOUT . '/header.php';
?>
<form class="auth-form" method="post" action="">
<?=CSRF::html();?>
<div class="auth-header">
<h1>Register</h1>
</div>
<?php if(isset($error) || isset($message)) { ?>
<div class="auth-message<?php if(isset($error)) { echo ' auth-message-error'; }?>"><?=($error ?? $message);?></div>
<?php } ?>
<label class="auth-field">
<div class="auth-field-name">Username</div>
<div class="auth-field-value"><input type="text" name="username" value="<?=htmlentities($username ?? '');?>"/></div>
</label>
<label class="auth-field">
<div class="auth-field-name">Password</div>
<div class="auth-field-value"><input type="password" name="password"/></div>
</label>
<label class="auth-field">
<div class="auth-field-name">Confirm Password</div>
<div class="auth-field-value"><input type="password" name="password_confirm"/></div>
</label>
<label class="auth-field">
<div class="auth-field-name">E-mail</div>
<div class="auth-field-value"><input type="email" name="email" value="<?=htmlentities($email ?? '');?>"/></div>
</label>
<label class="auth-field">
<div class="auth-field-name">Write "forum.flash.moe" backwards</div>
<div class="auth-field-value"><input type="text" name="<?=$antiSpam;?>"/></div>
</label>
<div class="auth-buttons">
<input type="submit" value="Register"/>
</div>
</form>
<?php
include FMF_LAYOUT . '/footer.php';

58
public/search.php Normal file
View file

@ -0,0 +1,58 @@
<?php
require_once '../startup.php';
$title = 'Search';
$hideSearch = true;
$query = isset($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : '';
$hasQuery = !empty($query);
if($hasQuery) {
$findTopics = $pdo->prepare('
SELECT t.*
FROM `fmf_topics` AS t
WHERE t.`topic_title` LIKE CONCAT("%", LOWER(:title), "%")
');
$findTopics->bindValue('title', $query);
$topics = $findTopics->execute() ? $findTopics->fetchAll(PDO::FETCH_ASSOC) : [];
$findPosts = $pdo->prepare('
SELECT p.*, t.`topic_title`, u.`user_login`
FROM `fmf_posts` AS p
LEFT JOIN `fmf_topics` AS t
ON t.`topic_id` = p.`topic_id`
LEFT JOIN `fmf_users` AS u
ON u.`user_id` = p.`user_id`
WHERE p.`post_text` LIKE CONCAT("%", LOWER(:text), "%")
AND p.`post_type` = 0
AND p.`post_deleted` IS NULL
');
$findPosts->bindValue('text', $query);
$posts = $findPosts->execute() ? $findPosts->fetchAll(PDO::FETCH_ASSOC) : [];
}
include FMF_LAYOUT . '/header.php';
?>
<form method="get" action="" class="search-form">
<input type="search" class="search-input" name="q" value="<?=$query;?>"/>
<input type="submit" class="search-submit" value="Search"/>
</form>
<?php
if($hasQuery) {
printf('<span style="font-size: .9em;">Found %d topics and %d posts.</span>', count($topics), count($posts));
echo '<h3>Topics</h3>';
foreach($topics as $topic) {
?>
<a href="/topic/<?=$topic['topic_id'];?>"><?=htmlentities($topic['topic_title']);?></a><br/>
<?php
}
echo '<h3>Posts</h3>';
foreach($posts as $post) {
?>
<a href="/post/<?=$post['post_id'];?>">Re: <?=htmlentities($post['topic_title']);?> by <?=($post['user_login'] ?? 'Deleted User');?> #<?=$post['post_id'];?></a><br/>
<?php
}
}
include FMF_LAYOUT . '/footer.php';

207
public/settings.php Normal file
View file

@ -0,0 +1,207 @@
<?php
require_once '../startup.php';
include_once '_user.php';
if(!session_active()) {
header('Location: /login?m=forbidden');
return;
}
$options = [
FMF_UF_SCROLLBEYOND => 'Scroll beyond end of the page.',
];
$timeZones = DateTimeZone::listIdentifiers();
if(isset($_POST['date_format_custom'], $_POST['timezone']) && CSRF::verify()) {
$timeZone = is_string($_POST['timezone']) ? $_POST['timezone'] : '';
$dateFormatCustom = is_string($_POST['date_format_custom']) ? $_POST['date_format_custom'] : '';
$currentPass = isset($_POST['currpass']) && is_string($_POST['currpass']) ? $_POST['currpass'] : '';
$newPass = isset($_POST['newpwd']) && is_string($_POST['newpwd']) ? $_POST['newpwd'] : '';
$confPass = isset($_POST['conpwd']) && is_string($_POST['conpwd']) ? $_POST['conpwd'] : '';
$newMail = isset($_POST['newmail']) && is_string($_POST['newmail']) ? $_POST['newmail'] : '';
$confMail = isset($_POST['conmail']) && is_string($_POST['conmail']) ? $_POST['conmail'] : '';
$setMail = !empty($newMail) && !empty($confMail);
$setPass = !empty($newPass) && !empty($confPass);
if($setMail || $setPass) {
if(!verify_password($currentPass)) {
$error = 'Current password was invalid.';
} else {
if(!isset($error) && $setPass) {
$error = validate_password($newPass);
if(!isset($error)) {
if($newPass !== $confPass) {
$email = 'Passwords don\'t match.';
} else {
user_set_password(current_user_id(), $newPass);
}
}
}
if(!isset($error) && $setMail) {
$error = validate_email($newMail);
if(!isset($error)) {
if($newMail !== $confMail) {
$error = 'E-mail addresses don\'t match.';
} else {
$emailVerification = user_set_email(current_user_id(), $newMail);
}
}
}
}
}
if(!isset($error)) {
if(!in_array($timeZone, $timeZones)) {
$error = 'Invalid time zone specified.';
} elseif(strlen($dateFormatCustom) > 50) {
$error = 'Invalid date/time format string.';
} else {
$userFlags = 0;
foreach(array_keys($options) as $flag)
if(!empty($_POST['flag_' . $flag]))
$userFlags |= $flag;
$updateUser = $pdo->prepare('
UPDATE `fmf_users`
SET `user_date_format` = :dtf,
`user_time_zone` = :tz,
`user_flags` = :flags
WHERE `user_id` = :user
');
$updateUser->bindValue('dtf', htmlentities($dateFormatCustom));
$updateUser->bindValue('tz', $timeZone);
$updateUser->bindValue('flags', $userFlags);
$updateUser->bindValue('user', current_user_id());
$updateUser->execute();
}
}
if(!empty($emailVerification)) {
$userInfo = user_info(current_user_id(), true);
$mailer->send(
(new Swift_Message('flash.moe message board activation'))
->setFrom(['system@flash.moe' => 'flash.moe'])
->setTo([$userInfo['user_email'] => $userInfo['user_login']])
->setBody(
"Hey {$userInfo['user_login']},\r\n\r\n".
"You are required to reactivate your account after e-mail changes.\r\n\r\n".
"Click the following link to activate your account:\r\n\r\n".
"<https://{$_SERVER['HTTP_HOST']}/activate/{$emailVerification}>\r\n"
)
);
destroy_session($_COOKIE['fmfauth'] ?? '');
header('Location: /login?m=reactivate');
return;
}
}
$userInfo = user_info(current_user_id(), true);
$title = 'Settings';
foreach($timeZones as $key => $timeZone) {
$timeZones[$key] = new DateTimeZone($timeZone);
$timeZones[$key]->offset = $timeZones[$key]->getOffset(new DateTime('now', new DateTimeZone('UTC')));
}
uasort($timeZones, function($a, $b) {
$diff = $a->offset <=> $b->offset;
if($diff === 0)
return strcmp($a->getName(), $b->getName());
return $diff;
});
include FMF_LAYOUT . '/header.php';
?>
<form method="post" action="">
<?=CSRF::html();?>
<?php if(isset($error) || isset($message)) { ?>
<div class="settings-message<?php if(isset($error)) { echo ' settings-message-error'; }?>"><?=($error ?? $message);?></div>
<?php } ?>
<div class="setting">
<div class="setting-head"><h3>Avatar</h3></div>
<div class="setting-value">
<a href="https://en.gravatar.com/">Gravatar</a> is used for user profile images, go <a href="https://en.gravatar.com/emails/">here</a> to change it. Only images with G rating will be used.
</div>
</div>
<div class="setting">
<div class="setting-head"><h3>Options</h3></div>
<div class="setting-value">
<?php
foreach($options as $oFlag => $oText) {
?>
<div class="settings-option"><label>
<input type="checkbox" name="flag_<?=$oFlag;?>" <?php if(($userInfo['user_flags'] & $oFlag) > 0) { echo 'checked'; } ?>/>
<?=$oText;?>
</label></div>
<?php
}
?>
</div>
</div>
<div class="setting">
<div class="setting-head"><h3>Date/time format</h3></div>
<div class="setting-value">
<input type="text" name="date_format_custom" value="<?=$userInfo['user_date_format'];?>"/><br/>
<a href="https://www.php.net/manual/en/function.date.php" style="font-size: .9em;" target="_blank" rel="noopener">Using PHP date() format</a>
</div>
</div>
<div class="setting">
<div class="setting-head"><h3>Time zone</h3></div>
<div class="setting-value">
<select name="timezone">
<?php
foreach($timeZones as $timeZone) {
?>
<option value="<?=$timeZone->getName();?>"<?=($timeZone->getName() === $userInfo['user_time_zone'] ? 'selected' : '');?>>(UTC<?=($timeZone->offset < 0 ? '-' : '+');?><?=gmdate('H:i', abs($timeZone->offset));?>) <?=$timeZone->getName();?></option>
<?php
}
?>
</select>
</div>
</div>
<div class="setting">
<div class="setting-head"><h3>Password</h3></div>
<div class="setting-value">
<label>New Password:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<input type="password" name="newpwd"/></label><br/>
<label>Confirm Password:&nbsp;<input type="password" name="conpwd"/></label>
</div>
</div>
<div class="setting">
<div class="setting-head"><h3>E-mail</h3></div>
<div class="setting-value">
<span style="font-size: .9em; font-weight: 700;">You will be forced to reactivate your account after changing your e-mail address, make sure to get it right!</span><br/>
<label>New e-mail address:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<input type="email" name="newmail" value="<?=$userInfo['user_email'];?>"/></label><br/>
<label>Confirm e-mail address:&nbsp;<input type="email" name="conmail"/></label>
</div>
</div>
<div class="setting">
<div class="setting-head"><h3>Current Password</h3></div>
<div class="setting-value">
Only required for changing e-mail or password.<br/>
<input type="password" name="currpass"/>
</div>
</div>
<div class="settings-buttons">
<input type="submit" value="Save"/>
<input type="reset" value="Reset"/>
</div>
</form>
<?php
include FMF_LAYOUT . '/footer.php';

476
public/style.css Normal file
View file

@ -0,0 +1,476 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
position: relative;
outline-style: none;
}
html,
body {
width: 100%;
height: 100%;
}
body {
font-family: Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
color: #9DAAD9;
background-color: #0F1528;
font-size: 12px;
line-height: 20px;
}
h1 {
color: #9DAAD9;
font-weight: bold;
font-size: 22px;
text-decoration: none;
}
h2 {
font-weight: bold;
font-size: 22px;
text-decoration: none;
line-height: 120%;
}
h3 {
font-size: 1.3em;
font-weight: bold;
line-height: 120%;
}
h4 {
margin: 0;
font-size: 1.1em;
font-weight: bold;
}
p {
font-size: 1.1em;
}
a {
color: #567194;
text-decoration: none;
transition: color .1s;
}
a:active {
color: #4C5A8E;
text-decoration: none;
}
a:hover {
color: #FFFFFF;
text-decoration: underline;
}
h3 > a {
display: inline-block;
padding: 2px 5px;
}
input[type="text"],
input[type="password"],
input[type="email"],
input[type="search"],
input[type="submit"],
input[type="reset"],
input[type="button"] {
color: #9daad9;
font-family: Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
font-size: 12px;
font-weight: 400;
padding: 2px;
border: 1px solid #9daad9;
background-color: #191E33;
}
input[type="button"],
input[type="reset"],
input[type="submit"] {
cursor: pointer;
padding: 2px 4px;
}
input[type="submit"] {
font-weight: 700;
}
textarea {
background-color: #191E33;
color: #9DAAD9;
font-family: Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
font-weight: normal;
border: 1px solid #A9B8C2;
padding: 2px;
font-size: 12px;
}
select {
color: #9DAAD9;
background-color: #191E33;
font-family: Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
font-size: 11px;
font-weight: normal;
border: 1px solid #A9B8C2;
padding: 1px;
}
option {
padding: 0 1em 0 0;
}
.wrapper {
margin: 0 auto;
max-width: 1000px;
width: 100%;
}
.wrapper.scrollbeyond {
padding-bottom: 100vh;
}
.footer {
color: #567194;
font-weight: normal;
font-size: 11px;
line-height: 14px;
text-align: center;
padding: 2px 5px;
}
.header h1 {
padding: 10px;
padding-bottom: 5px;
}
.header a {
color: #567194;
text-decoration: none;
padding: 5px 10px;
display: inline-block;
}
.header a:hover {
color: #9DAAD9;
text-decoration: underline;
}
.header-wrap {
display: flex;
justify-content: space-between;
}
.forum-category,
.topics {
border: 1px solid #000;
background-color: #191e33;
color: #cecece;
margin: 4px 0;
}
.forum-category-title,
.topics-header {
font-weight: 700;
display: flex;
align-items: center;
}
.forum-category-title-info,
.topics-header-info {
flex: 1 1 auto;
padding: 4px 5px;
}
.forum-category-board,
.topics-item {
border-top: 1px solid #000;
background-color: #1a2237;
display: flex;
align-items: center;
min-height: 40px;
}
.forum-category-board-indicator,
.topics-item-indicator,
.topics-item-indicator-closed,
.topics-item-indicator-locked {
width: 20px;
height: 20px;
line-height: 20px;
border: 1px solid #000;
flex: 0 0 auto;
margin: 5px;
background-color: #1a2237;
}
.topics-item-indicator-closed {
background-color: #0c0;
margin-left: 0;
}
.topics-item-indicator-locked {
background-color: #c00;
margin-left: 0;
}
.forum-category-board-indicator.unread,
.topics-item-indicator.unread {
background-color: #4D556A;
}
.topics-item-indicator,
.topics-item-indicator-closed,
.topics-item-indicator-locked {
width: 16px;
height: 16px;
line-height: 16px;
}
.forum-category-board-info,
.topics-item-info {
flex: 1 1 auto;
}
.forum-category-count,
.topics-item-count {
width: 60px;
text-align: center;
padding: 4px 5px;
}
.forum-category-latest,
.topics-item-author,
.topics-item-created,
.topics-item-latest {
width: 120px;
text-align: center;
}
.topics-item-created time,
.forum-category-latest time,
.topics-item-latest time {
font-size: .9em;
line-height: 1.2em;
display: inline-block;
}
.forum-category-latest-header,
.topics-item-latest-header {
padding: 4px 5px;
}
.forum-category-board-desc {
font-size: .9em;
line-height: 1.2em;
}
.topics-item-status {
display: inline-block;
margin-right: 4px;
}
.auth-form {
max-width: 300px;
margin: 5px auto;
}
.auth-header {
padding: 5px 0;
}
.auth-field {
margin: 5px 0;
display: block;
}
.auth-field-value input {
width: 100%;
}
.auth-buttons {
text-align: center;
padding: 5px;
}
.auth-message {
text-align: center;
}
.auth-message-error {
color: #f00;
}
.forum-title {
padding: 2px 5px;
}
.posting-header {
display: flex;
padding: 4px 0;
}
.posting-title {
flex: 1 1 auto;
}
.posting-submit {
margin-left: 4px;
}
.posting-text {
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
min-height: 500px;
}
.posting-message {
padding: 0 3px;
}
.posting-message-error {
color: #f00;
}
.createtopicbtn,
.topic-btns a {
display: inline-block;
color: #9daad9;
font-size: 12px;
font-weight: 400;
margin: 4px 6px 2px 0;
padding: 1px 8px;
border: 1px solid #9daad9;
background-color: #191E33;
cursor: pointer;
}
.search-form {
display: flex;
}
.search-input {
flex: 1 1 auto;
}
.search-submit {
margin-left: 4px;
}
.post {
display: flex;
border: 1px solid #000;
background-color: #191e33;
color: #cecece;
margin: 2px 0;
}
.post-details {
flex: 0 0 auto;
width: 120px;
display: flex;
flex-direction: column;
align-items: center;
border: 0 solid #000;
border-right-width: 1px;
}
.post-permalink-wrap {
text-align: center;
font-size: .9em;
line-height: 1.4em;
}
.post-username {
font-size: 1.2em;
line-height: 1.5em;
}
.post-details * {
margin: 1px 0;
}
/*@media(max-width: 600px) {*/
.post {
flex-direction: column;
}
.post-details {
border-right-width: 0;
border-bottom-width: 1px;
width: 100%;
flex-direction: row;
}
.post-avatar {
width: 40px;
height: 40px;
order: 1;
margin: 5px;
}
.post-username {
order: 2;
padding: 10px;
}
.post-permalink-wrap {
order: 3;
flex: 1 1 auto;
text-align: right !important;
}
.post-permalink {
padding: 10px;
}
/*}*/
.post-text {
flex: 1 1 auto;
background-color: #1a2237;
min-height: 160px;
padding: 2px 5px;
display: flex;
flex-direction: column;
}
.post-text-inner {
flex: 1 1 auto;
padding: 3px 0;
word-wrap: normal;
word-break: break-word;
}
.post-footer {
display: flex;
flex: 0 0 auto;
font-size: .9em;
}
.post-edited {
flex: 1 1 auto;
font-style: italic;
}
.post-options {
flex: 0 0 auto;
}
.post-options a {
margin: 0 5px;
}
.post-deleted .post-text {
min-height: 0;
}
.setting {
margin: 10px 0;
}
.setting-head h3 {
line-height: 1.5em;
}
.setting-value input[type="text"],
.setting-value input[type="password"],
.setting-value input[type="email"],
.setting-value select {
min-width: 400px;
margin: 1px 0;
}
.settings-message {
padding: 0 3px;
}
.settings-message-error {
color: #f00;
}
.settings-option {
margin: 2px 0;
padding: 0 4px;
display: block;
}
.event {
display: block;
border: 1px solid #000;
background-color: #191e33;
color: #cecece;
margin: 2px 0;
}
.event-msg {
margin: 4px;
display: flex;
}
.event-msg img {
vertical-align: bottom;
}
.event-msg time {
font-size: .9em;
margin: 0 4px;
}
.event-msg-text {
flex: 1 1 auto;
margin: 0 4px;
}
.notice {
display: block;
border: 1px solid #000;
background-color: #1a2237;
color: #cecece;
margin: 2px 0;
padding: 4px;
}

277
public/topic.php Normal file
View file

@ -0,0 +1,277 @@
<?php
require_once '../startup.php';
include_once '_category.php';
include_once '_topics.php';
include_once '_posts.php';
include_once '_user.php';
include_once '_track.php';
$topicId = isset($_GET['id']) && is_string($_GET['id']) && ctype_digit($_GET['id']) ? (int)$_GET['id'] : 0;
$topicInfo = topic_info($topicId);
if(empty($topicInfo['topic_bumped'])) {
die_ex('Topic not found.', 404);
}
$categoryInfo = category_info($topicInfo['cat_id']);
$isFeatureReqs = $categoryInfo['cat_variation'] == 1;
$userInfo = user_info(current_user_id());
$userActive = !empty($userInfo);
$isLocked = !empty($topicInfo['topic_locked']);
$isResolved = !empty($topicInfo['topic_resolved']);
$isConfirmed = !empty($topicInfo['topic_confirmed']);
$canLock = $userActive && $userInfo['user_moderator'];
$canResolve = $userActive && ($userInfo['user_moderator'] || (!$isLocked && $isFeatureReqs == 0 && $userInfo['user_id'] == $topicInfo['user_id']));
$canConfirm = $userActive && !$isResolved && $userInfo['user_moderator'];
if(isset($_GET['m']) && is_string($_GET['m'])) {
if(CSRF::verify()) {
switch($_GET['m']) {
case 'resolve':
if(!$canResolve)
die_ex('You aren\'t allowed to mark this topic as resolved.', 403);
if($isResolved)
die_ex('This topic is already marked as resolved.', 400);
$eventType = FMF_POST_TYPE_RESOLVE;
mark_topic_resolved($topicInfo['topic_id'], true);
$satoriMsg = "[b]forum.flash.moe[/b]: [url=https://forum.flash.moe/user/{$userInfo['user_id']}][b]{$userInfo['user_login']}[/b][/url] marked [url=https://forum.flash.moe/topic/{$topicId}][b]{$topicInfo['topic_title']}[/b][/url] as [b]resolved[/b].";
break;
case 'unresolve':
if(!$canResolve)
die_ex('You aren\'t allowed to mark this topic as unresolved.', 403);
if(!$isResolved)
die_ex('This topic is already marked as unresolved.', 400);
$eventType = FMF_POST_TYPE_UNRESOLVED;
$eventBump = true;
mark_topic_resolved($topicInfo['topic_id'], false);
$satoriMsg = "[b]forum.flash.moe[/b]: [url=https://forum.flash.moe/user/{$userInfo['user_id']}][b]{$userInfo['user_login']}[/b][/url] marked [url=https://forum.flash.moe/topic/{$topicId}][b]{$topicInfo['topic_title']}[/b][/url] as [b]unresolved[/b].";
break;
case 'confirm':
if(!$canConfirm)
die_ex('You aren\'t allowed to mark this topic as confirmed.', 403);
if($isResolved)
die_ex('This topic has been marked as resolved and doesn\'t need to be confirmed anymore.', 400);
if($isConfirmed)
die_ex('This topic is already marked as confirmed.', 400);
$eventType = FMF_POST_TYPE_CONFIRMED;
$eventBump = true;
mark_topic_confirmed($topicInfo['topic_id'], true);
$satoriMsg = "[b]forum.flash.moe[/b]: [url=https://forum.flash.moe/user/{$userInfo['user_id']}][b]{$userInfo['user_login']}[/b][/url] marked [url=https://forum.flash.moe/topic/{$topicId}][b]{$topicInfo['topic_title']}[/b][/url] as [b]confirmed[/b].";
break;
case 'unconfirm':
if(!$canConfirm)
die_ex('You aren\'t allowed to mark this topic as unconfirmed.', 403);
if($isResolved)
die_ex('This topic has been marked as resolved, you can\'t revoke confirmations anymore.', 400);
if(!$isConfirmed)
die_ex('This topic is not confirmed.', 400);
$eventType = FMF_POST_TYPE_UNCONFIRMED;
$eventBump = true;
mark_topic_confirmed($topicInfo['topic_id'], false);
$satoriMsg = "[b]forum.flash.moe[/b]: [url=https://forum.flash.moe/user/{$userInfo['user_id']}][b]{$userInfo['user_login']}[/b][/url] marked [url=https://forum.flash.moe/topic/{$topicId}][b]{$topicInfo['topic_title']}[/b][/url] as [b]unconfirmed[/b].";
break;
case 'lock':
if(!$canLock)
die_ex('You aren\'t allowed to lock this topic.', 403);
if($isLocked)
die_ex('This topic is already locked.', 400);
$eventType = FMF_POST_TYPE_LOCKED;
lock_topic($topicInfo['topic_id'], true);
break;
case 'unlock':
if(!$canLock)
die_ex('You aren\'t allowed to unlock this topic.', 403);
if(!$isLocked)
die_ex('This topic is already unlocked.', 400);
$eventType = FMF_POST_TYPE_UNLOCKED;
lock_topic($topicInfo['topic_id'], false);
break;
default:
die_ex('Unknown action.', 404);
break;
}
if(isset($eventType))
create_topic_event($topicInfo['cat_id'], $topicInfo['topic_id'], $userInfo['user_id'], $eventType, $eventData ?? null);
if(isset($eventBump))
topic_bump($topicInfo['topic_id']);
}
if(defined('SATORI_SECRET') && !empty($satoriMsg)) {
$sock = @fsockopen(SATORI_HOST, SATORI_PORT, $errno, $errstr, 2);
if($sock) {
fwrite($sock, chr(0xF) . hash_hmac('sha256', $satoriMsg, SATORI_SECRET) . $satoriMsg . chr(0xF));
fflush($sock);
fclose($sock);
}
}
header('Location: /topic/'. $topicInfo['topic_id']);
return;
}
if($userActive)
update_track($userInfo['user_id'], $topicInfo['topic_id'], $topicInfo['cat_id']);
$title = $topicInfo['topic_title'];
$posts = posts_in_topic($topicInfo['topic_id']);
include FMF_LAYOUT . '/header.php';
$breadcrumbs = '<a href="/">forum.flash.moe</a> &raquo; ';
$breadcrumbs_arr = category_breadcrumbs($topicInfo['cat_id'], false);
foreach($breadcrumbs_arr as $breadcrumb) {
$breadcrumbs .= sprintf('<a href="/category/%d">%s</a> &raquo; ', $breadcrumb['cat_id'], $breadcrumb['cat_name']);
}
echo $breadcrumbs;
$topicButtons = '<div class="topic-btns">';
if(!$isLocked || $canLock) {
$topicButtons .= '<a href="/topic/'. $topicInfo['topic_id'] .'/reply">Reply</a>';
}
if($canResolve) {
$topicButtons .= '<a href="/topic/'. $topicInfo['topic_id'] .'/'. ($isResolved ? 'unresolve' : 'resolve') .'?_csrf='. CSRF::token() .'">'. ($isResolved ? 'Reopen' : ($isFeatureReqs ? 'Mark Implemented' : 'Mark Resolved')) .'</a>';
}
if($canConfirm) {
$topicButtons .= '<a href="/topic/'. $topicInfo['topic_id'] .'/'. ($isConfirmed ? 'unconfirm' : 'confirm') .'?_csrf='. CSRF::token() .'">'. ($isConfirmed ? ($isFeatureReqs ? 'Revoke Accept' : 'Revoke Confirmation') : ($isFeatureReqs ? 'Accept for implementation' : 'Confirm')) .'</a>';
}
if($canLock) {
$topicButtons .= '<a href="/topic/'. $topicInfo['topic_id'] .'/'. ($isLocked ? 'unlock' : 'lock') .'?_csrf='. CSRF::token() .'">'. ($isLocked ? 'Unlock' : 'Lock') .'</a>';
}
$topicButtons .= '</div>';
?>
<h3 class="forum-title"><?=htmlentities($topicInfo['topic_title']);?></h3>
<?php
echo $topicButtons;
if($isLocked) {
?>
<div class="notice">
This topic was <b>locked</b> on <b><time datetime="<?=date('c', $topicInfo['topic_locked']);?>"><?=date(FMF_DATE_FORMAT, $topicInfo['topic_locked']);?></time></b>.
</div>
<?php
}
if($isResolved) {
?>
<div class="notice">
This topic was marked <b>resolved</b> on <b><time datetime="<?=date('c', $topicInfo['topic_resolved']);?>"><?=date(FMF_DATE_FORMAT, $topicInfo['topic_resolved']);?></time></b>.
</div>
<?php
}
foreach($posts as $post) {
$authorInfo = user_info($post['user_id']);
if($post['post_type'] == FMF_POST_TYPE_MESSAGE) {
?>
<div class="post<?=(empty($post['post_deleted']) ? '' : ' post-deleted')?>" id="p<?=$post['post_id'];?>">
<?php if(empty($post['post_deleted'])) { ?>
<div class="post-details">
<a class="post-username" href="/user/<?=($authorInfo['user_id'] ?? 0);?>"><?=($authorInfo['user_login'] ?? 'Deleted User');?></a>
<img class="post-avatar" src="<?=user_gravatar($authorInfo['user_id'] ?? null);?>" alt="<?=($authorInfo['user_login'] ?? 'Deleted User');?>"/>
<div class="post-permalink-wrap">
<a class="post-permalink" href="/post/<?=$post['post_id'];?>"><time class="post-time" datetime="<?=date('c', $post['post_created']);?>"><?=date(FMF_DATE_FORMAT, $post['post_created']);?></time></a>
</div>
</div>
<?php } ?>
<div class="post-text">
<?php if(empty($post['post_deleted'])) { ?>
<div class="post-text-inner">
<?=(new Parsedown)->setSafeMode(true)->text($post['post_text']);?>
</div>
<div class="post-footer">
<div class="post-edited">
<?php if(!empty($post['post_edited'])) { ?>
Last edited: <time datetime="<?=date('c', $post['post_edited']);?>"><?=date(FMF_DATE_FORMAT, $post['post_edited']);?></time>
<?php } ?>
</div>
<div class="post-options">
<?php if($userActive && ($userInfo['user_moderator'] || $userInfo['user_id'] === $post['user_id'])) { ?>
<a href="/post/<?=$post['post_id'];?>/edit">Edit</a>
<a href="/post/<?=$post['post_id'];?>?m=delete&amp;_csrf=<?=CSRF::token();?>">Delete</a>
<?php } ?>
<?php if(!empty($post['user_id']) && $userActive && $userInfo['user_moderator']) { ?>
<a href="/post/<?=$post['post_id'];?>?m=anonymize&amp;_csrf=<?=CSRF::token();?>">Strip User ID</a>
<?php } ?>
</div>
</div>
<?php } else { ?>
<div class="post-text-inner">
<a href="/post/<?=$post['post_id'];?>">#<?=$post['post_id'];?></a>: <code>deleted</code>
<?php if($userActive && $userInfo['user_moderator']) { ?>
(<a href="/post/<?=$post['post_id'];?>?m=restore&amp;_csrf=<?=CSRF::token();?>">Restore</a>)
<?php } ?>
</div>
<?php } ?>
</div>
</div>
<?php
} else {
unset($eventHtml);
switch($post['post_type']) {
default:
$eventMessages = [
FMF_POST_TYPE_RESOLVE => '<a href="/user/:user_id">:username</a> has marked this ' . ($isFeatureReqs ? 'request as <b>implemented</b>.' : 'topic as <b>resolved</b>.'),
FMF_POST_TYPE_LOCKED => '<a href="/user/:user_id">:username</a> has <b>locked</b> this conversation.',
FMF_POST_TYPE_UNLOCKED => '<a href="/user/:user_id">:username</a> has <b>unlocked</b> this conversation.',
FMF_POST_TYPE_UNRESOLVED => '<a href="/user/:user_id">:username</a> has marked this ' . ($isFeatureReqs ? 'request as <b>unimplemented</b>.' : 'marked this topic as <b>unresolved</b>.'),
FMF_POST_TYPE_CONFIRMED => '<a href="/user/:user_id">:username</a> has ' . ($isFeatureReqs ? 'marked this request for <b>implementation</b>.' : '<b>confirmed</b> this topic.'),
FMF_POST_TYPE_UNCONFIRMED => '<a href="/user/:user_id">:username</a> has ' . ($isFeatureReqs ? '<b>revoked implementation confirmation</b> for this request.' : '<b>revoked the confirmation</b> from this topic.'),
];
$eventMessage = $eventMessages[$post['post_type']] ?? '<a href="/user/:user_id">:username</a> did something.';
break;
}
if(isset($eventMessage)) {
$eventMessage = strtr($eventMessage, [
':user_id' => $authorInfo['user_id'] ?? 0,
':username' => $authorInfo['user_login'] ?? 'Deleted User',
]);
}
if(!isset($eventHtml)) {
$eventHtml = sprintf(
'<div class="event-msg"><img class="event-avatar" src="%s" alt="%s"/><div class="event-msg-text">%s</div><a class="event-permalink" href="/post/%d"><time class="event-time" datetime="%s">%s</time></a></div>',
user_gravatar($authorInfo['user_id'] ?? null, 20),
($authorInfo['user_login'] ?? 'Deleted User'),
$eventMessage ?? 'Missing event description.',
$post['post_id'],
date('c', $post['post_created']),
date(FMF_DATE_FORMAT, $post['post_created'])
);
}
?>
<div class="event" id="p<?=$post['post_id'];?>">
<?=$eventHtml;?>
</div>
<?php
}
}
echo $topicButtons;
echo $breadcrumbs . $topicInfo['topic_title'];
include FMF_LAYOUT . '/footer.php';

19
public/user.php Normal file
View file

@ -0,0 +1,19 @@
<?php
require_once '../startup.php';
include_once '_user.php';
$userId = isset($_GET['id']) && is_string($_GET['id']) && ctype_digit($_GET['id']) ? (int)$_GET['id'] : 0;
$userInfo = user_info($userId);
if(empty($userInfo))
die_ex('User not found.', 404);
$title = 'Profile of ' . $userInfo['user_login'];
include FMF_LAYOUT . '/header.php';
echo "<h3>{$userInfo['user_login']}</h3>";
echo '<img src="' . user_gravatar($userInfo['user_id']) . '" alt="' . $userInfo['user_login'] . '"/>';
include FMF_LAYOUT . '/footer.php';

77
startup.php Normal file
View file

@ -0,0 +1,77 @@
<?php
define('FMF_STARTUP', microtime(true));
define('FMF_ROOT', __DIR__);
define('FMF_DEBUG', is_file(FMF_ROOT . '/.debug'));
define('FMF_PHP_MIN_VER', '7.3.0');
define('FMF_LAYOUT', FMF_ROOT . '/layout');
define('FMF_INCLUDE', FMF_ROOT . '/include');
if(version_compare(PHP_VERSION, FMF_PHP_MIN_VER, '<')) {
die('At least PHP <b>' . FMF_PHP_MIN_VER . '</b> is required.');
}
error_reporting(FMF_DEBUG ? -1 : 0);
ini_set('display_errors', FMF_DEBUG ? 'On' : 'Off');
date_default_timezone_set('UTC');
mb_internal_encoding('UTF-8');
set_include_path(get_include_path() . PATH_SEPARATOR . FMF_INCLUDE);
if(!is_file(FMF_ROOT . '/config.php'))
die('Configuration is missing.');
require_once FMF_ROOT . '/vendor/autoload.php';
require_once FMF_ROOT . '/config.php';
try {
$pdo = new PDO(CHIE_DB_DSN, CHIE_DB_USER, CHIE_DB_PASS, [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION time_zone = '+00:00'"
. ", sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';",
]);
$mailer = new Swift_Mailer(
(new Swift_SmtpTransport(CHIE_SMTP_HOST, CHIE_SMTP_PORT))
->setEncryption(CHIE_SMTP_ENC)
->setUsername(CHIE_SMTP_USER)
->setPassword(CHIE_SMTP_PASS)
);
} catch(Exception $ex) {
die($ex->getMessage());
}
include_once '_csrf.php';
include_once '_user.php';
function die_ex(string $message, int $status = 400): void {
http_response_code($status);
include FMF_LAYOUT . '/notice.php';
exit;
}
CSRF::setGlobalSecretKey(CHIE_CSRF_SECRET);
purge_old_sessions();
session_activate($_COOKIE['fmfauth'] ?? '');
$userInfo = user_info(current_user_id());
if(!empty($userInfo)) {
define('FMF_DATE_FORMAT', $userInfo['user_date_format']);
date_default_timezone_set($userInfo['user_time_zone']);
CSRF::setGlobalIdentity($_COOKIE['fmfauth'] ?? '');
if(!empty($userInfo['user_banned'])) {
$banTimestamp = $userInfo['user_banned'];
$banReason = $userInfo['user_banned_reason'];
include FMF_LAYOUT . '/banned.php';
destroy_session($_COOKIE['fmfauth'] ?? '');
exit;
}
} else {
define('FMF_DATE_FORMAT', 'D Y-m-d H:i:s T');
CSRF::setGlobalIdentity($_SERVER['REMOTE_ADDR']);
}
unset($userInfo);