Browse Source

Import

master
flash 1 year ago
commit
4cab95baf5
  1. 1
      .gitattributes
  2. 3
      .gitignore
  3. 6
      composer.json
  4. 481
      composer.lock
  5. 21
      config.example.php
  6. 131
      include/_category.php
  7. 96
      include/_csrf.php
  8. 100
      include/_posts.php
  9. 78
      include/_topics.php
  10. 94
      include/_track.php
  11. 239
      include/_user.php
  12. 17
      include/_utils.php
  13. 10
      layout/banned.php
  14. 15
      layout/footer.php
  15. 48
      layout/header.php
  16. 6
      layout/notice.php
  17. 4
      public/404.php
  18. 9
      public/activate.php
  19. 132
      public/category.php
  20. 26
      public/hook/github.php
  21. BIN
      public/images/accept.png
  22. BIN
      public/images/bomb.png
  23. BIN
      public/images/cross.png
  24. BIN
      public/images/delete.png
  25. BIN
      public/images/error.png
  26. BIN
      public/images/lock.png
  27. BIN
      public/images/star.png
  28. BIN
      public/images/thumb_down.png
  29. BIN
      public/images/thumb_up.png
  30. BIN
      public/images/tick.png
  31. 54
      public/index.php
  32. 71
      public/login.php
  33. 11
      public/logout.php
  34. 35
      public/post.php
  35. 151
      public/posting.php
  36. 114
      public/register.php
  37. 58
      public/search.php
  38. 207
      public/settings.php
  39. 476
      public/style.css
  40. 277
      public/topic.php
  41. 19
      public/user.php
  42. 77
      startup.php

1
.gitattributes

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

3
.gitignore

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

6
composer.json

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

481
composer.lock

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

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

17
include/_utils.php

@ -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

@ -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

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

48
layout/header.php

@ -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

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

4
public/404.php

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

9
public/activate.php

@ -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

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

26
public/hook/github.php

@ -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

After

Width: 16  |  Height: 16  |  Size: 781 B

BIN
public/images/bomb.png

After

Width: 16  |  Height: 16  |  Size: 793 B

BIN
public/images/cross.png

After

Width: 16  |  Height: 16  |  Size: 655 B

BIN
public/images/delete.png

After

Width: 16  |  Height: 16  |  Size: 715 B

BIN
public/images/error.png

After

Width: 16  |  Height: 16  |  Size: 666 B

BIN
public/images/lock.png

After

Width: 16  |  Height: 16  |  Size: 749 B

BIN
public/images/star.png

After

Width: 16  |  Height: 16  |  Size: 670 B

BIN
public/images/thumb_down.png

After

Width: 16  |  Height: 16  |  Size: 601 B

BIN
public/images/thumb_up.png

After

Width: 16  |  Height: 16  |  Size: 619 B

BIN
public/images/tick.png

After

Width: 16  |  Height: 16  |  Size: 537 B

54
public/index.php

@ -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>