Import
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto
|
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/config.php
|
||||
/.debug
|
||||
/vendor
|
6
composer.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"require": {
|
||||
"swiftmailer/swiftmailer": "^6.0",
|
||||
"erusev/parsedown": "^1.7"
|
||||
}
|
||||
}
|
481
composer.lock
generated
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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 .'&r=g&d=identicon';
|
||||
}
|
17
include/_utils.php
Normal 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
|
@ -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
|
@ -0,0 +1,15 @@
|
|||
<div class="footer">
|
||||
Powered by Chie<br/>
|
||||
© <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
|
@ -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
|
@ -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
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
require_once '../startup.php';
|
||||
|
||||
die_ex('Page not found.', 404);
|
9
public/activate.php
Normal 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
|
@ -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> » ';
|
||||
foreach($breadcrumbs_arr as $breadcrumb) {
|
||||
$breadcrumbs .= sprintf('<a href="/category/%d">%s</a> » ', $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
|
@ -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
After Width: | Height: | Size: 781 B |
BIN
public/images/bomb.png
Normal file
After Width: | Height: | Size: 793 B |
BIN
public/images/cross.png
Normal file
After Width: | Height: | Size: 655 B |
BIN
public/images/delete.png
Normal file
After Width: | Height: | Size: 715 B |
BIN
public/images/error.png
Normal file
After Width: | Height: | Size: 666 B |
BIN
public/images/lock.png
Normal file
After Width: | Height: | Size: 749 B |
BIN
public/images/star.png
Normal file
After Width: | Height: | Size: 670 B |
BIN
public/images/thumb_down.png
Normal file
After Width: | Height: | Size: 601 B |
BIN
public/images/thumb_up.png
Normal file
After Width: | Height: | Size: 619 B |
BIN
public/images/tick.png
Normal file
After Width: | Height: | Size: 537 B |
54
public/index.php
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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> » ';
|
||||
foreach($breadcrumbs as $breadcrumb)
|
||||
printf('<a href="/category/%d">%s</a> » ', $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
|
@ -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
|
@ -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
|
@ -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: <input type="password" name="newpwd"/></label><br/>
|
||||
<label>Confirm Password: <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: <input type="email" name="newmail" value="<?=$userInfo['user_email'];?>"/></label><br/>
|
||||
<label>Confirm e-mail address: <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
|
@ -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
|
@ -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> » ';
|
||||
$breadcrumbs_arr = category_breadcrumbs($topicInfo['cat_id'], false);
|
||||
foreach($breadcrumbs_arr as $breadcrumb) {
|
||||
$breadcrumbs .= sprintf('<a href="/category/%d">%s</a> » ', $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&_csrf=<?=CSRF::token();?>">Delete</a>
|
||||
<?php } ?>
|
||||
<?php if(!empty($post['user_id']) && $userActive && $userInfo['user_moderator']) { ?>
|
||||
<a href="/post/<?=$post['post_id'];?>?m=anonymize&_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&_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
|
@ -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
|
@ -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);
|