Massive overhauls.

This commit is contained in:
flash 2019-12-12 01:42:28 +01:00
parent 51f3c47a31
commit b4a581087a
50 changed files with 1080 additions and 614 deletions

View file

@ -1,8 +1,11 @@
<?php
namespace Misuzu;
use Misuzu\Database\{ Database, DatabaseMigrationManager };
use PDO;
use Misuzu\Database\Database;
use Misuzu\Database\DatabaseMigrationManager;
use Misuzu\Net\GeoIP;
use Misuzu\Net\IPAddress;
define('MSZ_STARTUP', microtime(true));
define('MSZ_ROOT', __DIR__);
@ -37,7 +40,6 @@ $errorHandler->register();
require_once 'src/audit_log.php';
require_once 'src/changelog.php';
require_once 'src/colour.php';
require_once 'src/comments.php';
require_once 'src/manage.php';
require_once 'src/news.php';
@ -50,9 +52,6 @@ require_once 'src/Forum/poll.php';
require_once 'src/Forum/post.php';
require_once 'src/Forum/topic.php';
require_once 'src/Forum/validate.php';
require_once 'src/Net/geoip.php';
require_once 'src/Net/ip.php';
require_once 'src/Parsers/parse.php';
require_once 'src/Users/auth.php';
require_once 'src/Users/avatar.php';
require_once 'src/Users/background.php';
@ -384,7 +383,7 @@ MIG;
exit;
}
geoip_init(Config::get('geoip.database', Config::TYPE_STR, '/var/lib/GeoIP/GeoLite2-Country.mmdb'));
GeoIP::init(Config::get('geoip.database', Config::TYPE_STR, '/var/lib/GeoIP/GeoLite2-Country.mmdb'));
if(!MSZ_DEBUG) {
$twigCache = sys_get_temp_dir() . '/msz-tpl-cache-' . md5(MSZ_ROOT);
@ -456,7 +455,7 @@ MIG;
}
CSRF::setGlobalSecretKey(Config::get('csrf.secret', Config::TYPE_STR, 'soup'));
CSRF::setGlobalIdentity(empty($userDisplayInfo) ? ip_remote_address() : $cookieData['session_token']);
CSRF::setGlobalIdentity(empty($userDisplayInfo) ? IPAddress::remote() : $cookieData['session_token']);
if(Config::get('private.enabled', Config::TYPE_BOOL)) {
$onLoginPage = $_SERVER['PHP_SELF'] === url('auth-login');

View file

@ -1,6 +1,7 @@
<?php
namespace Misuzu;
use Misuzu\Net\IPAddress;
use Misuzu\Users\User;
require_once '../../misuzu.php';
@ -20,7 +21,7 @@ $notices = [];
$siteIsPrivate = Config::get('private.enable', Config::TYPE_BOOL);
$loginPermCat = $siteIsPrivate ? Config::get('private.perm.cat', Config::TYPE_STR) : '';
$loginPermVal = $siteIsPrivate ? Config::get('private.perm.val', Config::TYPE_INT) : 0;
$ipAddress = ip_remote_address();
$ipAddress = IPAddress::remote();
$remainingAttempts = user_login_attempts_remaining($ipAddress);
while(!empty($_POST['login']) && is_array($_POST['login'])) {

View file

@ -2,6 +2,7 @@
namespace Misuzu;
use UnexpectedValueException;
use Misuzu\Net\IPAddress;
require_once '../../misuzu.php';
@ -25,7 +26,7 @@ if($userId > 0 && empty($username)) {
$notices = [];
$siteIsPrivate = Config::get('private.enable', Config::TYPE_BOOL);
$canResetPassword = $siteIsPrivate ? Config::get('private.allow_password_reset', Config::TYPE_BOOL, true) : true;
$ipAddress = ip_remote_address();
$ipAddress = IPAddress::remote();
$remainingAttempts = user_login_attempts_remaining($ipAddress);
while($canResetPassword) {

View file

@ -1,6 +1,8 @@
<?php
namespace Misuzu;
use Misuzu\Net\IPAddress;
use Misuzu\Net\IPAddressBlacklist;
use Misuzu\Users\User;
require_once '../../misuzu.php';
@ -12,10 +14,10 @@ if(user_session_active()) {
$register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : [];
$notices = [];
$ipAddress = ip_remote_address();
$ipAddress = IPAddress::remote();
$remainingAttempts = user_login_attempts_remaining($ipAddress);
$restricted = ip_blacklist_check(ip_remote_address()) ? 'blacklist'
: (user_warning_check_ip(ip_remote_address()) ? 'ban' : '');
$restricted = IPAddressBlacklist::check($ipAddress) ? 'blacklist'
: (user_warning_check_ip($ipAddress) ? 'ban' : '');
while(!$restricted && !empty($register)) {
if(!CSRF::validateRequest()) {

View file

@ -1,6 +1,8 @@
<?php
namespace Misuzu;
use Misuzu\Net\IPAddress;
require_once '../../misuzu.php';
if(user_session_active()) {
@ -10,7 +12,7 @@ if(user_session_active()) {
$twofactor = !empty($_POST['twofactor']) && is_array($_POST['twofactor']) ? $_POST['twofactor'] : [];
$notices = [];
$ipAddress = ip_remote_address();
$ipAddress = IPAddress::remote();
$remainingAttempts = user_login_attempts_remaining($ipAddress);
$tokenInfo = user_auth_tfa_token_info(
!empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : (

View file

@ -1,6 +1,9 @@
<?php
namespace Misuzu;
use Misuzu\Net\IPAddress;
use Misuzu\Parsers\Parser;
require_once '../../misuzu.php';
if(!user_session_active()) {
@ -40,13 +43,13 @@ if($mode === 'preview') {
$postText = (string)($_POST['post']['text']);
$postParser = (int)($_POST['post']['parser']);
if(!parser_is_valid($postParser)) {
if(!Parser::isValid($postParser)) {
http_response_code(400);
return;
}
http_response_code(200);
echo parse_text(htmlspecialchars($postText), $postParser);
echo Parser::instance($postParser)->parseText(htmlspecialchars($postText));
return;
}
@ -129,7 +132,7 @@ $notices = [];
if(!empty($_POST)) {
$topicTitle = $_POST['post']['title'] ?? '';
$postText = $_POST['post']['text'] ?? '';
$postParser = (int)($_POST['post']['parser'] ?? MSZ_PARSER_BBCODE);
$postParser = (int)($_POST['post']['parser'] ?? Parser::BBCODE);
$topicType = isset($_POST['post']['type']) ? (int)$_POST['post']['type'] : null;
$postSignature = isset($_POST['post']['signature']);
@ -170,7 +173,7 @@ if(!empty($_POST)) {
}
}
if(!parser_is_valid($postParser)) {
if(!Parser::isValid($postParser)) {
$notices[] = 'Invalid parser selected.';
}
@ -202,7 +205,7 @@ if(!empty($_POST)) {
$topicId,
$forum['forum_id'],
user_session_current('user_id', 0),
ip_remote_address(),
IPAddress::remote(),
$postText,
$postParser,
$postSignature
@ -212,7 +215,7 @@ if(!empty($_POST)) {
break;
case 'edit':
if(!forum_post_update($postId, ip_remote_address(), $postText, $postParser, $postSignature, $postText !== $post['post_text'])) {
if(!forum_post_update($postId, IPAddress::remote(), $postText, $postParser, $postSignature, $postText !== $post['post_text'])) {
$notices[] = 'Post edit failed.';
}

View file

@ -1,6 +1,8 @@
<?php
namespace Misuzu;
use Misuzu\Net\IPAddressBlacklist;
require_once '../../../misuzu.php';
if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), General::PERM_MANAGE_BLACKLIST)) {
@ -18,7 +20,7 @@ if(!empty($_POST)) {
if(!empty($_POST['blacklist']['remove']) && is_array($_POST['blacklist']['remove'])) {
foreach($_POST['blacklist']['remove'] as $cidr) {
if(!ip_blacklist_remove($cidr)) {
if(!IPAddressBlacklist::remove($cidr)) {
$notices[] = sprintf('Failed to remove "%s" from the blacklist.', $cidr);
}
}
@ -34,7 +36,7 @@ if(!empty($_POST)) {
continue;
}
if(!ip_blacklist_add($cidr)) {
if(!IPAddressBlacklist::add($cidr)) {
$notices[] = sprintf('Failed to add "%s" to the blacklist.', $cidr);
}
}
@ -44,5 +46,5 @@ if(!empty($_POST)) {
Template::render('manage.general.blacklist', [
'notices' => $notices,
'blacklist' => ip_blacklist_list(),
'blacklist' => IPAddressBlacklist::list(),
]);

View file

@ -45,21 +45,20 @@ if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()
return;
}
$roleColour = colour_create();
$roleColour = new Colour;
if(!empty($_POST['role']['colour']['inherit'])) {
colour_set_inherit($roleColour);
$roleColour->setInherit(true);
} else {
foreach(['red', 'green', 'blue'] as $key) {
$value = (int)($_POST['role']['colour'][$key] ?? -1);
$func = 'colour_set_' . ucfirst($key);
if($value < 0 || $value > 0xFF) {
echo 'invalid colour value';
try {
$roleColour->{'set' . ucfirst($key)}($value);
} catch(\Exception $ex){
echo $ex->getMessage();
return;
}
$func($roleColour, $value);
}
}
@ -118,7 +117,7 @@ if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()
$updateRole->bind('role_name', $roleName);
$updateRole->bind('role_hierarchy', $roleHierarchy);
$updateRole->bind('role_hidden', $roleSecret ? 1 : 0);
$updateRole->bind('role_colour', $roleColour);
$updateRole->bind('role_colour', $roleColour->getRaw());
$updateRole->bind('role_description', $roleDescription);
$updateRole->bind('role_title', $roleTitle);
$updateRole->execute();
@ -179,7 +178,10 @@ if($roleId !== null) {
return;
}
Template::set(['edit_role' => $editRole]);
Template::set([
'edit_role' => $editRole,
'role_colour' => new Colour($editRole['role_colour']),
]);
}
Template::render('manage.users.role', [

View file

@ -139,17 +139,19 @@ if(CSRF::validateRequest() && $canEdit) {
}
if(!empty($_POST['colour']) && is_array($_POST['colour'])) {
$userColour = null;
$setUserInfo['user_colour'] = null;
if(!empty($_POST['colour']['enable'])) {
$userColour = colour_create();
$userColour = new Colour;
if(!colour_from_hex($userColour, (string)($_POST['colour']['hex'] ?? ''))) {
$notices[] = 'An invalid colour was supplied.';
try {
$userColour->setHex((string)($_POST['colour']['hex'] ?? ''));
} catch(\Exception $ex) {
$notices[] = $ex->getMessage();
}
}
$setUserInfo['user_colour'] = $userColour;
$setUserInfo['user_colour'] = $userColour->getRaw();
}
}
if(!empty($_POST['password']) && is_array($_POST['password'])) {
@ -241,6 +243,7 @@ $roles = $getRoles->fetchAll();
Template::render('manage.users.user', [
'manage_user' => $manageUser,
'user_colour' => empty($manageUser['user_colour']) ? Colour::none() : new Colour($manageUser['user_colour']),
'manage_notices' => $notices,
'manage_roles' => $roles,
'can_edit_user' => $canEdit,

View file

@ -1,6 +1,8 @@
<?php
namespace Misuzu;
use Misuzu\Net\IPAddress;
require_once '../../../misuzu.php';
if(!perms_check_user(MSZ_PERMS_USER, user_session_current('user_id'), MSZ_PERM_USER_MANAGE_WARNINGS)) {
@ -77,7 +79,7 @@ if(!empty($_POST['warning']) && is_array($_POST['warning'])) {
$warningsUser,
user_get_last_ip($warningsUser),
$currentUserId,
ip_remote_address(),
IPAddress::remote(),
$warningType,
$_POST['warning']['note'],
$_POST['warning']['private'],

View file

@ -16,7 +16,7 @@ $feedMode = trim($_SERVER['PATH_INFO'] ?? '', '/');
$categoryId = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0;
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
if(!empty($feedMode) && news_feed_supported($feedMode)) {
if(!empty($feedMode) && in_array($feedMode, ['rss', 'atom'])) {
$location = empty($categoryId) ? url("news-feed-{$feedMode}") : url("news-category-feed-{$feedMode}", ['category' => $categoryId]);
}

View file

@ -1,11 +1,26 @@
<?php
namespace Misuzu;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\Parsers\Parser;
require_once '../../misuzu.php';
$feedMode = trim($_SERVER['PATH_INFO'] ?? '', '/');
if(!news_feed_supported($feedMode)) {
switch($feedMode) {
case 'rss':
$feedSerializer = new RssFeedSerializer;
break;
case 'atom':
$feedSerializer = new AtomFeedSerializer;
break;
}
if(!isset($feedSerializer)) {
echo render_error(400);
return;
}
@ -28,11 +43,34 @@ if(!$posts) {
return;
}
$feed = (new Feed)
->setTitle(Config::get('site.name', Config::TYPE_STR, 'Misuzu') . ' » ' . ($category['category_name'] ?? 'Featured News'))
->setDescription($category['category_description'] ?? 'A live featured news feed.')
->setContentUrl(url_prefix(false) . (empty($category) ? url('news-index') : url('news-category', ['category' => $category['category_id']])))
->setFeedUrl(url_prefix(false) . (empty($category) ? url("news-feed-{$feedMode}") : url("news-category-feed-{$feedMode}", ['category' => $category['category_id']])));
foreach($posts as $post) {
$postUrl = url_prefix(false) . url('news-post', ['post' => $post['post_id']]);
$commentsUrl = url_prefix(false) . url('news-post-comments', ['post' => $post['post_id']]);
$authorUrl = url_prefix(false) . url('user-profile', ['user' => $post['user_id']]);
$feedItem = (new FeedItem)
->setTitle($post['post_title'])
->setSummary(first_paragraph($post['post_text']))
->setContent(Parser::instance(Parser::MARKDOWN)->parseText($post['post_text']))
->setCreationDate(strtotime($post['post_created']))
->setUniqueId($postUrl)
->setContentUrl($postUrl)
->setCommentsUrl($commentsUrl)
->setAuthorName($post['username'])
->setAuthorUrl($authorUrl);
if(!$feed->hasLastUpdate() || $feed->getLastUpdate() < $feedItem->getCreationDate())
$feed->setLastUpdate($feedItem->getCreationDate());
$feed->addItem($feedItem);
}
header("Content-Type: application/{$feedMode}+xml; charset=utf-8");
echo news_feed($feedMode, $posts, [
'title' => Config::get('site.name', Config::TYPE_STR, 'Misuzu') . ' » ' . ($category['category_name'] ?? 'Featured News'),
'subtitle' => $category['category_description'] ?? 'A live featured news feed.',
'html-url' => empty($category) ? url('news-index') : url('news-category', ['category' => $category['category_id']]),
'feed-url' => empty($category) ? url("news-feed-{$feedMode}") : url("news-category-feed-{$feedMode}", ['category' => $category['category_id']]),
]);
echo $feedSerializer->serializeFeed($feed);

View file

@ -1,6 +1,7 @@
<?php
namespace Misuzu;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
require_once '../misuzu.php';
@ -88,7 +89,7 @@ if($isEditing) {
$setAboutError = user_set_about_page(
$profileUser->user_id,
$_POST['about']['text'] ?? '',
(int)($_POST['about']['parser'] ?? MSZ_PARSER_PLAIN)
(int)($_POST['about']['parser'] ?? Parser::PLAIN)
);
if($setAboutError !== MSZ_E_USER_ABOUT_OK) {
@ -107,7 +108,7 @@ if($isEditing) {
$setSignatureError = user_set_signature(
$profileUser->user_id,
$_POST['signature']['text'] ?? '',
(int)($_POST['signature']['parser'] ?? MSZ_PARSER_PLAIN)
(int)($_POST['signature']['parser'] ?? Parser::PLAIN)
);
if($setSignatureError !== MSZ_E_USER_SIGNATURE_OK) {

View file

@ -48,8 +48,8 @@ final class CSRF {
public static function validateRequest(?string $identity = null, ?string $secretKey = null): bool {
if(isset($_SERVER['HTTP_X_MISUZU_CSRF'])) {
$token = $_SERVER['HTTP_X_MISUZU_CSRF'];
} elseif(isset($_POST['_csrf']) && is_string($_POST['_csrf'])) {
$token = $_POST['_csrf'];
} elseif(isset($_REQUEST['_csrf']) && is_string($_REQUEST['_csrf'])) { // Change this to $_POST later, it should never appear in urls
$token = $_REQUEST['_csrf'];
} elseif(isset($_REQUEST['csrf']) && is_string($_REQUEST['csrf'])) {
$token = $_REQUEST['csrf'];
} else {

144
src/Colour.php Normal file
View file

@ -0,0 +1,144 @@
<?php
namespace Misuzu;
use InvalidArgumentException;
class Colour {
private const FLAG_INHERIT = 0x40000000;
private const READABILITY_THRESHOLD = 186;
private const LUMINANCE_WEIGHT_RED = .299;
private const LUMINANCE_WEIGHT_GREEN = .587;
private const LUMINANCE_WEIGHT_BLUE = .114;
private int $raw = 0;
public function __construct(?int $raw = 0) {
$this->setRaw($raw);
}
public static function none(): self {
return new static(self::FLAG_INHERIT);
}
public static function fromRgb(int $red, int $green, int $blue): self {
return (new static)->setRed($red)->getGreen($green)->setBlue($blue);
}
public static function fromHex(string $hex): self {
return (new static)->setHex($hex);
}
public function getRaw(): int {
return $this->raw;
}
public function setRaw(int $raw): self {
if($raw < 0 || $raw > 0x7FFFFFFF)
throw new InvalidArgumentException('Invalid raw colour.');
$this->raw = $raw;
return $this;
}
public function getInherit(): bool {
return ($this->getRaw() & self::FLAG_INHERIT) > 0;
}
public function setInherit(bool $inherit): self {
$raw = $this->getRaw();
if($inherit)
$raw |= self::FLAG_INHERIT;
else
$raw &= ~self::FLAG_INHERIT;
$this->setRaw($raw);
return $this;
}
public function getRed(): int {
return ($this->getRaw() & 0xFF0000) >> 16;
}
public function setRed(int $red): self {
if($red < 0 || $red > 0xFF)
throw new InvalidArgumentException('Invalid red value.');
$raw = $this->getRaw();
$raw &= ~0xFF0000;
$raw |= $red << 16;
$this->setRaw($raw);
return $this;
}
public function getGreen(): int {
return ($this->getRaw() & 0xFF00) >> 8;
}
public function setGreen(int $green): self {
if($green < 0 || $green > 0xFF)
throw new InvalidArgumentException('Invalid green value.');
$raw = $this->getRaw();
$raw &= ~0xFF00;
$raw |= $green << 8;
$this->setRaw($raw);
return $this;
}
public function getBlue(): int {
return ($this->getRaw() & 0xFF);
}
public function setBlue(int $blue): self {
if($blue < 0 || $blue > 0xFF)
throw new InvalidArgumentException('Invalid blue value.');
$raw = $this->getRaw();
$raw &= ~0xFF;
$raw |= $blue;
$this->setRaw($raw);
return $this;
}
public function getLuminance(): int {
return self::LUMINANCE_WEIGHT_RED * $this->getRed()
+ self::LUMINANCE_WEIGHT_GREEN * $this->getGreen()
+ self::LUMINANCE_WEIGHT_BLUE * $this->getBlue();
}
public function getHex(): string {
return str_pad(dechex($this->getRaw() & 0xFFFFFF), 6, '0', STR_PAD_LEFT);
}
public function setHex(string $hex): self {
if($hex[0] === '#')
$hex = mb_substr($hex, 1);
if(!ctype_xdigit($hex))
throw new InvalidArgumentException('Argument contains invalid characters.');
$length = mb_strlen($hex);
if($length === 3) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
} elseif($length !== 6) {
throw new InvalidArgumentException('Argument is not a hex string.');
}
return $this->setRaw(hexdec($hex));
}
public function getCSS(): string {
if($this->getInherit())
return 'inherit';
return '#' . $this->getHex();
}
public static function extractCSSContract(
string $dark = 'dark', string $light = 'light', bool $inheritIsDark = true
): string {
if($this->getInherit())
return $inheritIsDark ? $dark : $light;
return $this->getLuminance($colour) > self::READABILITY_THRESHOLD ? $dark : $light;
}
}

54
src/Debug/Stopwatch.php Normal file
View file

@ -0,0 +1,54 @@
<?php
namespace Misuzu\Debug;
class Stopwatch {
private $startTime = 0;
private $stopTime = 0;
private $laps = [];
private static $instance = null;
public static function __callStatic(string $name, array $args) {
if(self::$instance === null)
self::$instance = new static;
return self::$instance->{substr($name, 1)}(...$args);
}
public function __construct() {}
private static function time() {
return microtime(true);
}
public function start(): void {
$this->startTime = self::time();
}
public function lap(string $text): void {
$this->laps[$text] = self::time();
}
public function stop(): void {
$this->stopTime = self::time();
}
public function reset(): void {
$this->laps = [];
$this->startTime = 0;
$this->stopTime = 0;
}
public function elapsed(): float {
return $this->stopTime - $this->startTime;
}
public function laps(): array {
$laps = [];
foreach($this->laps as $name => $time) {
$laps[$name] = $time - $this->startTime;
}
return $laps;
}
}

View file

@ -0,0 +1,115 @@
<?php
namespace Misuzu\Feeds;
use DOMDocument;
use DOMElement;
class AtomFeedSerializer extends XmlFeedSerializer {
protected function formatTime(int $time): string {
return date('c', $time);
}
protected function createRoot(DOMDocument $document, Feed $feed): DOMElement {
$atom = $document->appendChild($document->createElement('feed'));
$atom->setAttribute('xmlns', 'http://www.w3.org/2005/Atom');
$atom->appendChild(
$document->createElement(
'id',
$feed->hasContentUrl()
? $this->cleanString($feed->getContentUrl())
: time()
)
);
return $atom;
}
protected function createTitle(DOMDocument $document, string $title): DOMElement {
return $document->createElement('title', $this->cleanString($title));
}
protected function createDescription(DOMDocument $document, string $description): ?DOMElement {
return $document->createElement('subtitle', $this->cleanString($description));
}
protected function createLastUpdate(DOMDocument $document, int $lastUpdate): ?DOMElement {
return $document->createElement('updated', $this->formatTime($lastUpdate));
}
protected function createContentUrl(DOMDocument $document, string $contentUrl): ?DOMElement {
$link = $document->createElement('link');
$link->setAttribute('href', $this->cleanString($contentUrl));
return $link;
}
protected function createFeedUrl(DOMDocument $document, string $feedUrl): ?DOMElement {
$link = $document->createElement('link');
$link->setAttribute('href', $this->cleanString($feedUrl));
$link->setAttribute('ref', 'self');
return $link;
}
protected function createItem(DOMDocument $document, FeedItem $feedItem): DOMElement {
$elem = $document->createElement('entry');
$elem->appendChild(
$document->createElement(
'id',
$feedItem->hasContentUrl()
? $this->cleanString($feedItem->getContentUrl())
: time()
)
);
return $elem;
}
protected function createItemTitle(DOMDocument $document, string $title): DOMElement {
return $document->createElement('title', $this->cleanString($title));
}
protected function createItemSummary(DOMDocument $document, string $summary): ?DOMElement {
return $document->createElement('summary', $this->cleanString($summary));
}
protected function createItemContent(DOMDocument $document, string $content): ?DOMElement {
$elem = $document->createElement('content', $this->cleanString($content));
$elem->setAttribute('type', 'html');
return $elem;
}
protected function createItemCreationDate(DOMDocument $document, int $creationDate): ?DOMElement {
return $document->createElement('updated', $this->formatTime($creationDate));
}
protected function createItemUniqueId(DOMDocument $document, string $uniqueId): ?DOMElement {
return null;
}
protected function createItemContentUrl(DOMDocument $document, string $contentUrl): ?DOMElement {
$elem = $document->createElement('link');
$elem->setAttribute('href', $this->cleanString($contentUrl));
$elem->setAttribute('type', 'text/html');
return $elem;
}
protected function createItemCommentsUrl(DOMDocument $document, string $commentsUrl): ?DOMElement {
return null;
}
protected function createItemAuthor(DOMDocument $document, ?string $authorName, ?string $authorUrl): ?DOMElement {
if(empty($authorName) && empty($authorUrl))
return null;
$elem = $document->createElement('author');
if(!empty($authorName))
$elem->appendChild($document->createElement('name', $this->cleanString($authorName)));
if(!empty($authorUrl))
$elem->appendChild($document->createElement('uri', $this->cleanString($authorUrl)));
return $elem;
}
}

78
src/Feeds/Feed.php Normal file
View file

@ -0,0 +1,78 @@
<?php
namespace Misuzu\Feeds;
use InvalidArgumentException;
class Feed {
private string $title = '';
private ?string $description = null;
private ?int $lastUpdate = null;
private ?string $contentUrl = null;
private ?string $feedUrl = null;
private array $feedItems = [];
public function getTitle(): string {
return $this->title;
}
public function setTitle(string $title): self {
$this->title = $title;
return $this;
}
public function getDescription(): string {
return $this->description ?? '';
}
public function hasDescription(): bool {
return isset($this->description);
}
public function setDescription(?string $description): self {
$this->description = $description;
return $this;
}
public function getLastUpdate(): int {
return $this->lastUpdate ?? 0;
}
public function hasLastUpdate(): bool {
return isset($this->lastUpdate);
}
public function setLastUpdate(?int $lastUpdate): self {
$this->lastUpdate = $lastUpdate;
return $this;
}
public function getContentUrl(): string {
return $this->contentUrl ?? '';
}
public function hasContentUrl(): bool {
return isset($this->contentUrl);
}
public function setContentUrl(?string $contentUrl): self {
$this->contentUrl = $contentUrl;
return $this;
}
public function getFeedUrl(): string {
return $this->feedUrl ?? '';
}
public function hasFeedUrl(): bool {
return isset($this->feedUrl);
}
public function setFeedUrl(?string $feedUrl): self {
$this->feedUrl = $feedUrl;
return $this;
}
public function getItems(): array {
return $this->feedItems;
}
public function hasItems(): bool {
return count($this->feedItems) > 0;
}
public function addItem(FeedItem $item): self {
if($item === null)
throw new InvalidArgumentException('item may not be null');
$this->feedItems[] = $item;
return $this;
}
}

110
src/Feeds/FeedItem.php Normal file
View file

@ -0,0 +1,110 @@
<?php
namespace Misuzu\Feeds;
class FeedItem {
private string $title = '';
private ?string $summary = null;
private ?string $content = null;
private ?int $creationDate = null;
private ?string $uniqueId = null;
private ?string $contentUrl = null;
private ?string $commentsUrl = null;
private ?string $authorName = null;
private ?string $authorUrl = null;
public function getTitle(): string {
return $this->title;
}
public function setTitle(string $title): self {
$this->title = $title;
return $this;
}
public function getSummary(): string {
return $this->summary ?? '';
}
public function hasSummary(): bool {
return isset($this->summary);
}
public function setSummary(?string $summary): self {
$this->summary = $summary;
return $this;
}
public function getContent(): string {
return $this->content ?? '';
}
public function hasContent(): bool {
return isset($this->content);
}
public function setContent(?string $content): self {
$this->content = $content;
return $this;
}
public function getCreationDate(): int {
return $this->creationDate;
}
public function hasCreationDate(): bool {
return isset($this->creationDate);
}
public function setCreationDate(?int $creationDate): self {
$this->creationDate = $creationDate;
return $this;
}
public function getUniqueId(): string {
return $this->uniqueId ?? '';
}
public function hasUniqueId(): bool {
return isset($this->uniqueId);
}
public function setUniqueId(?string $uniqueId): self {
$this->uniqueId = $uniqueId;
return $this;
}
public function getContentUrl(): string {
return $this->contentUrl ?? '';
}
public function hasContentUrl(): bool {
return isset($this->contentUrl);
}
public function setContentUrl(?string $contentUrl): self {
$this->contentUrl = $contentUrl;
return $this;
}
public function getCommentsUrl(): string {
return $this->commentsUrl ?? '';
}
public function hasCommentsUrl(): bool {
return isset($this->commentsUrl);
}
public function setCommentsUrl(?string $commentsUrl): self {
$this->commentsUrl = $commentsUrl;
return $this;
}
public function getAuthorName(): string {
return $this->authorName ?? '';
}
public function hasAuthorName(): bool {
return isset($this->authorName);
}
public function setAuthorName(?string $authorName): self {
$this->authorName = $authorName;
return $this;
}
public function getAuthorUrl(): string {
return $this->authorUrl ?? '';
}
public function hasAuthorUrl(): bool {
return isset($this->authorUrl);
}
public function setAuthorUrl(?string $authorUrl): self {
$this->authorUrl = $authorUrl;
return $this;
}
}

View file

@ -0,0 +1,6 @@
<?php
namespace Misuzu\Feeds;
abstract class FeedSerializer {
abstract public function serializeFeed(Feed $feed): string;
}

View file

@ -0,0 +1,82 @@
<?php
namespace Misuzu\Feeds;
use DOMDocument;
use DOMElement;
class RssFeedSerializer extends XmlFeedSerializer {
protected function formatTime(int $time): string {
return date('r', $time);
}
protected function createRoot(DOMDocument $document, Feed $feed): DOMElement {
$rss = $document->appendChild($document->createElement('rss'));
$rss->setAttribute('version', '2.0');
$rss->setAttribute('xmlns:atom', 'http://www.w3.org/2005/Atom');
$channel = $rss->appendChild($document->createElement('channel'));
$channel->appendChild($document->createElement('ttl', '900'));
return $channel;
}
protected function createTitle(DOMDocument $document, string $title): DOMElement {
return $document->createElement('title', $this->cleanString($title));
}
protected function createDescription(DOMDocument $document, string $description): ?DOMElement {
return $document->createElement('description', $this->cleanString($description));
}
protected function createLastUpdate(DOMDocument $document, int $lastUpdate): ?DOMElement {
return $document->createElement('pubDate', $this->formatTime($lastUpdate));
}
protected function createContentUrl(DOMDocument $document, string $contentUrl): ?DOMElement {
return $document->createElement('link', $this->cleanString($contentUrl));
}
protected function createFeedUrl(DOMDocument $document, string $feedUrl): ?DOMElement {
$link = $document->createElement('atom:link');
$link->setAttribute('href', $this->cleanString($feedUrl));
$link->setAttribute('ref', 'self');
return $link;
}
protected function createItem(DOMDocument $document, FeedItem $feedItem): DOMElement {
return $document->createElement('item');
}
protected function createItemTitle(DOMDocument $document, string $title): DOMElement {
return $document->createElement('title', $this->cleanString($title));
}
protected function createItemSummary(DOMDocument $document, string $summary): ?DOMElement {
return $document->createElement('description', $this->cleanString($summary));
}
protected function createItemContent(DOMDocument $document, string $content): ?DOMElement {
return null;
}
protected function createItemCreationDate(DOMDocument $document, int $creationDate): ?DOMElement {
return $document->createElement('pubDate', $this->formatTime($creationDate));
}
protected function createItemUniqueId(DOMDocument $document, string $uniqueId): ?DOMElement {
$elem = $document->createElement('guid', $uniqueId);
$elem->setAttribute('isPermaLink', 'true');
return $elem;
}
protected function createItemContentUrl(DOMDocument $document, string $contentUrl): ?DOMElement {
return $document->createElement('link', $contentUrl);
}
protected function createItemCommentsUrl(DOMDocument $document, string $commentsUrl): ?DOMElement {
return $document->createElement('comments', $commentsUrl);
}
protected function createItemAuthor(DOMDocument $document, ?string $authorName, ?string $authorUrl): ?DOMElement {
return null;
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Misuzu\Feeds;
use DOMDocument;
use DOMElement;
abstract class XmlFeedSerializer extends FeedSerializer {
public function serializeFeed(Feed $feed): string {
$document = new DOMDocument('1.0', 'utf-8');
$root = $this->createRoot($document, $feed);
$root->appendChild($this->createTitle($document, $feed->getTitle()));
if($feed->hasDescription())
self::appendChild($root, $this->createDescription($document, $feed->getDescription()));
if($feed->hasLastUpdate())
self::appendChild($root, $this->createLastUpdate($document, $feed->getLastUpdate()));
if($feed->hasContentUrl())
self::appendChild($root, $this->createContentUrl($document, $feed->getContentUrl()));
if($feed->hasFeedUrl())
self::appendChild($root, $this->createFeedUrl($document, $feed->getFeedUrl()));
if($feed->hasItems()) {
foreach($feed->getItems() as $item) {
$root->appendChild($this->serializeFeedItem($document, $item));
}
}
return $document->saveXML();
}
private function serializeFeedItem(DOMDocument $document, FeedItem $feedItem): DOMElement {
$elem = $this->createItem($document, $feedItem);
$elem->appendChild($this->createItemTitle($document, $feedItem->getTitle()));
if($feedItem->hasSummary())
self::appendChild($elem, $this->createItemSummary($document, $feedItem->getSummary()));
if($feedItem->hasContent())
self::appendChild($elem, $this->createItemContent($document, $feedItem->getContent()));
if($feedItem->hasCreationDate())
self::appendChild($elem, $this->createItemCreationDate($document, $feedItem->getCreationDate()));
if($feedItem->hasUniqueId())
self::appendChild($elem, $this->createItemUniqueId($document, $feedItem->getUniqueId()));
if($feedItem->hasContentUrl())
self::appendChild($elem, $this->createItemContentUrl($document, $feedItem->getContentUrl()));
if($feedItem->hasCommentsUrl())
self::appendChild($elem, $this->createItemCommentsUrl($document, $feedItem->getCommentsUrl()));
if($feedItem->hasAuthorName() || $feedItem->hasAuthorUrl())
self::appendChild($elem, $this->createItemAuthor($document, $feedItem->getAuthorName(), $feedItem->getAuthorUrl()));
return $elem;
}
protected function cleanString(string $string): string {
return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT | ENT_SUBSTITUTE);
}
protected static function appendChild(DOMElement $parent, ?DOMElement $elem): ?DOMElement {
if($elem !== null)
return $parent->appendChild($elem);
return $elem;
}
abstract protected function formatTime(int $time): string;
abstract protected function createRoot(DOMDocument $document, Feed $feed): DOMElement;
abstract protected function createTitle(DOMDocument $document, string $title): DOMElement;
abstract protected function createDescription(DOMDocument $document, string $description): ?DOMElement;
abstract protected function createLastUpdate(DOMDocument $document, int $lastUpdate): ?DOMElement;
abstract protected function createContentUrl(DOMDocument $document, string $contentUrl): ?DOMElement;
abstract protected function createFeedUrl(DOMDocument $document, string $feedUrl): ?DOMElement;
abstract protected function createItem(DOMDocument $document, FeedItem $feedItem): DOMElement;
abstract protected function createItemTitle(DOMDocument $document, string $title): DOMElement;
abstract protected function createItemSummary(DOMDocument $document, string $summary): ?DOMElement;
abstract protected function createItemContent(DOMDocument $document, string $content): ?DOMElement;
abstract protected function createItemCreationDate(DOMDocument $document, int $creationDate): ?DOMElement;
abstract protected function createItemUniqueId(DOMDocument $document, string $uniqueId): ?DOMElement;
abstract protected function createItemContentUrl(DOMDocument $document, string $contentUrl): ?DOMElement;
abstract protected function createItemCommentsUrl(DOMDocument $document, string $commentsUrl): ?DOMElement;
abstract protected function createItemAuthor(DOMDocument $document, ?string $authorName, ?string $authorUrl): ?DOMElement;
}

View file

@ -226,7 +226,7 @@ function forum_get_colour(int $forumId): int {
$forumId = $colourInfo['forum_parent'];
}
return colour_none();
return 0x40000000;
}
function forum_increment_clicks(int $forumId): void {

View file

@ -7,7 +7,7 @@ function forum_post_create(
int $userId,
string $ipAddress,
string $text,
int $parser = MSZ_PARSER_PLAIN,
int $parser = \Misuzu\Parsers\Parser::PLAIN,
bool $displaySignature = true
): int {
$createPost = \Misuzu\DB::prepare('
@ -31,7 +31,7 @@ function forum_post_update(
int $postId,
string $ipAddress,
string $text,
int $parser = MSZ_PARSER_PLAIN,
int $parser = \Misuzu\Parsers\Parser::PLAIN,
bool $displaySignature = true,
bool $bumpUpdate = true
): bool {

23
src/Net/GeoIP.php Normal file
View file

@ -0,0 +1,23 @@
<?php
namespace Misuzu\Net;
use GeoIp2\Database\Reader as GeoIPDBReader;
final class GeoIP {
private static $geoip = null;
private static $geoipDbPath = null;
public static function init(string $dbPath): void {
self::$geoipDbPath = $dbPath;
}
public static function getReader(): GeoIPDBReader {
if(self::$geoip === null)
self::$geoip = new GeoIPDBReader(self::$geoipDbPath);
return self::$geoip;
}
public static function resolveCountry(string $ipAddress) {
return self::getReader()->country($ipAddress);
}
}

108
src/Net/IPAddress.php Normal file
View file

@ -0,0 +1,108 @@
<?php
namespace Misuzu\Net;
use Exception;
use InvalidArgumentException;
final class IPAddress {
public const VERSION_UNKNOWN = 0;
public const VERSION_4 = 4;
public const VERSION_6 = 6;
private const SIZES = [
self::VERSION_4 => 4,
self::VERSION_6 => 16,
];
public const DEFAULT_V4 = '127.1';
public const DEFAULT_V6 = '::1';
public static function remote(string $fallback = self::DEFAULT_V6): string {
return $_SERVER['REMOTE_ADDR'] ?? $fallback;
}
public static function country(string $address, string $fallback = 'XX'): string {
try {
return GeoIP::resolveCountry($address)->country->isoCode ?? $fallback;
} catch(Exception $e) {}
return $fallback;
}
public static function rawWidth(int $version): int {
return isset(self::SIZES[$version]) ? self::SIZES[$version] : 0;
}
public static function detectStringVersion(string $address): int {
if(filter_var($address, FILTER_VALIDATE_IP) !== false) {
if(filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false)
return self::VERSION_6;
if(filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false)
return self::VERSION_4;
}
return self::VERSION_UNKNOWN;
}
public static function detectRawVersion(string $address): int {
$addressLength = strlen($address);
foreach(self::SIZES as $version => $length) {
if($length === $addressLength)
return $version;
}
return self::VERSION_UNKNOWN;
}
public static function cidrToRaw(string $cidr): ?array {
if(strpos($cidr, '/') !== false) {
[$subnet, $mask] = explode('/', $cidr, 2);
} else {
$subnet = $cidr;
}
try {
$subnet = inet_pton($subnet);
} catch(Exception $ex) {
return null;
}
$mask = empty($mask) ? null : (int)$mask;
return compact('subnet', 'mask');
}
public static function matchCidr(string $address, string $cidr): bool {
$address = inet_pton($address);
$cidr = self::cidrToRaw($cidr);
return self::matchCidrRaw($address, $cidr['subnet'], $cidr['mask']);
}
public static function matchCidrRaw(string $address, string $subnet, ?int $mask = null): bool {
$version = self::detectRawVersion($subnet);
if($version === self::VERSION_UNKNOWN)
return false;
$bits = self::SIZES[$version] * 8;
if(empty($mask))
$mask = $bits;
if($mask < 1 || $mask > $bits || $version !== self::detectRawVersion($subnet))
return false;
for($i = 0; $i < ceil($mask / 8); $i++) {
$byteMask = (0xFF00 >> max(0, min(8, $mask - ($i * 8)))) & 0xFF;
$addressByte = ord($address[$i]) & $byteMask;
$subnetByte = ord($subnet[$i]) & $byteMask;
if($addressByte !== $subnetByte)
return false;
}
return true;
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace Misuzu\Net;
use Misuzu\DB;
final class IPAddressBlacklist {
public static function check(string $address): bool {
return (bool)DB::prepare("
SELECT INET6_ATON(:address) AS `target`, (
SELECT COUNT(*) > 0
FROM `msz_ip_blacklist`
WHERE LENGTH(`ip_subnet`) = LENGTH(`target`)
AND `ip_subnet` & LPAD('', LENGTH(`ip_subnet`), X'FF') << LENGTH(`ip_subnet`) * 8 - `ip_mask`
= `target` & LPAD('', LENGTH(`ip_subnet`), X'FF') << LENGTH(`ip_subnet`) * 8 - `ip_mask`
)
")->bind('address', $address)
->fetchColumn(1, false);
}
public static function add(string $cidr): bool {
$raw = IPAddress::cidrToRaw($cidr);
if(empty($raw))
return false;
return self::addRaw($raw['subnet'], $raw['mask']);
}
public static function addRaw(string $subnet, ?int $mask = null): bool {
$version = IPAddress::detectRawVersion($subnet);
if($version === IPAddress::VERSION_UNKNOWN)
return false;
$bits = IPAddress::rawWidth($version) * 8;
if(empty($mask)) {
$mask = $bits;
} elseif($mask < 1 || $mask > $bits) {
return false;
}
return DB::prepare('
REPLACE INTO `msz_ip_blacklist` (`ip_subnet`, `ip_mask`)
VALUES (:subnet, :mask)
')->bind('subnet', $subnet)
->bind('mask', $mask)
->execute();
}
public static function remove(string $cidr): bool {
$raw = IPAddress::cidrToRaw($cidr);
if(empty($raw))
return false;
return self::removeRaw($raw['subnet'], $raw['mask']);
}
public static function removeRaw(string $subnet, ?int $mask = null): bool {
return DB::prepare('
DELETE FROM `msz_ip_blacklist`
WHERE `ip_subnet` = :subnet
AND `ip_mask` = :mask
')->bind('subnet', $subnet)
->bind('mask', $mask)
->execute();
}
public static function list(): array {
return DB::query("
SELECT
INET6_NTOA(`ip_subnet`) AS `ip_subnet`,
`ip_mask`,
LENGTH(`ip_subnet`) AS `ip_bytes`,
CONCAT(INET6_NTOA(`ip_subnet`), '/', `ip_mask`) as `ip_cidr`
FROM `msz_ip_blacklist`
")->fetchAll();
}
}

View file

@ -1,38 +0,0 @@
<?php
use GeoIp2\Database\Reader as GeoIPDBReader;
function geoip_init(?string $database = null): void {
$existing = geoip_instance();
if(!empty($existing)) {
$existing->close();
}
geoip_instance(new GeoIPDBReader($database ?? \Misuzu\Config::get('geoip.database')));
}
function geoip_instance(?GeoIPDBReader $newInstance = null): ?GeoIPDBReader {
static $instance = null;
if(!empty($newInstance)) {
$instance = $newInstance;
}
return $instance;
}
function geoip_cache(string $section, string $ipAddress, callable $value) {
static $memo = [];
if(empty($meme[$ipAddress][$section])) {
$memo[$ipAddress][$section] = $value();
}
return $memo[$ipAddress][$section] ?? null;
}
function geoip_country(string $ipAddress) {
return geoip_cache('country', $ipAddress, function () use ($ipAddress) {
return geoip_instance()->country($ipAddress);
});
}

View file

@ -1,184 +0,0 @@
<?php
define('MSZ_IP_UNKNOWN', 0);
define('MSZ_IP_V4', 4);
define('MSZ_IP_V6', 6);
define('MSZ_IP_SIZES', [
MSZ_IP_V4 => 4,
MSZ_IP_V6 => 16,
]);
function ip_remote_address(string $fallback = '::1'): string {
return $_SERVER['REMOTE_ADDR'] ?? $fallback;
}
function ip_country_code(string $address, string $fallback = 'XX'): string {
try {
return geoip_country($address)->country->isoCode ?? $fallback;
} catch(Exception $e) {}
return $fallback;
}
function ip_get_string_version(string $address): int {
if(filter_var($address, FILTER_VALIDATE_IP) === false) {
return MSZ_IP_UNKNOWN;
}
if(filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) {
return MSZ_IP_V6;
}
if(filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) {
return MSZ_IP_V4;
}
return MSZ_IP_UNKNOWN;
}
function ip_get_raw_version(string $raw): int {
$rawLength = strlen($raw);
foreach(MSZ_IP_SIZES as $version => $length) {
if($rawLength === $length) {
return $version;
}
}
return MSZ_IP_UNKNOWN;
}
function ip_get_raw_width(int $version): int {
return MSZ_IP_SIZES[$version] ?? 0;
}
function ip_match_cidr_raw(string $address, string $subnet, ?int $mask = null): bool {
$version = ip_get_raw_version($subnet);
$bits = ip_get_raw_width($version) * 8;
if(empty($mask)) {
$mask = $bits;
}
if($mask < 1 || $mask > $bits || $version !== ip_get_raw_version($subnet)) {
return false;
}
for($i = 0; $i < ceil($mask / 8); $i++) {
$byteMask = (0xFF00 >> max(0, min(8, $mask - ($i * 8)))) & 0xFF;
$addressByte = ord($address[$i]) & $byteMask;
$subnetByte = ord($subnet[$i]) & $byteMask;
if($addressByte !== $subnetByte) {
return false;
}
}
return true;
}
function ip_match_cidr(string $address, string $cidr): bool {
$address = inet_pton($address);
[$subnet, $mask] = ['', 0];
extract(ip_cidr_to_raw($cidr));
return ip_match_cidr_raw($address, $subnet, $mask);
}
function ip_cidr_to_raw(string $cidr): ?array {
if(strpos($cidr, '/') !== false) {
[$subnet, $mask] = explode('/', $cidr, 2);
} else {
$subnet = $cidr;
}
try {
$subnet = inet_pton($subnet);
} catch(Exception $ex) {
return null;
}
$mask = empty($mask) ? null : (int)$mask;
return compact('subnet', 'mask');
}
function ip_blacklist_check(string $address): bool {
$checkBlacklist = \Misuzu\DB::prepare("
SELECT COUNT(*) > 0
FROM `msz_ip_blacklist`
WHERE LENGTH(`ip_subnet`) = LENGTH(INET6_ATON(:ip1))
AND `ip_subnet` & LPAD('', LENGTH(`ip_subnet`), X'FF') << LENGTH(`ip_subnet`) * 8 - `ip_mask`
= INET6_ATON(:ip2) & LPAD('', LENGTH(`ip_subnet`), X'FF') << LENGTH(`ip_subnet`) * 8 - `ip_mask`
");
$checkBlacklist->bind('ip1', $address);
$checkBlacklist->bind('ip2', $address);
return (bool)$checkBlacklist->fetchColumn();
}
function ip_blacklist_add_raw(string $subnet, ?int $mask = null): bool {
$version = ip_get_raw_version($subnet);
if($version === 0) {
return false;
}
$bits = ip_get_raw_width($version) * 8;
if(empty($mask)) {
$mask = $bits;
} elseif($mask < 1 || $mask > $bits) {
return false;
}
$addBlacklist = \Misuzu\DB::prepare('
REPLACE INTO `msz_ip_blacklist`
(`ip_subnet`, `ip_mask`)
VALUES
(:subnet, :mask)
');
$addBlacklist->bind('subnet', $subnet);
$addBlacklist->bind('mask', $mask);
return $addBlacklist->execute();
}
function ip_blacklist_add(string $cidr): bool {
$raw = ip_cidr_to_raw($cidr);
if(empty($raw)) {
return false;
}
return ip_blacklist_add_raw($raw['subnet'], $raw['mask']);
}
function ip_blacklist_remove_raw(string $subnet, ?int $mask = null): bool {
$removeBlacklist = \Misuzu\DB::prepare('
DELETE FROM `msz_ip_blacklist`
WHERE `ip_subnet` = :subnet
AND `ip_mask` = :mask
');
$removeBlacklist->bind('subnet', $subnet);
$removeBlacklist->bind('mask', $mask);
return $removeBlacklist->execute();
}
function ip_blacklist_remove(string $cidr): bool {
$raw = ip_cidr_to_raw($cidr);
if(empty($raw)) {
return false;
}
return ip_blacklist_remove_raw($raw['subnet'], $raw['mask']);
}
function ip_blacklist_list(): array {
return \Misuzu\DB::query("
SELECT
INET6_NTOA(`ip_subnet`) AS `ip_subnet`,
`ip_mask`,
LENGTH(`ip_subnet`) AS `ip_bytes`,
CONCAT(INET6_NTOA(`ip_subnet`), '/', `ip_mask`) as `ip_cidr`
FROM `msz_ip_blacklist`
")->fetchAll();
}

View file

@ -4,12 +4,6 @@ namespace Misuzu\Parsers\BBCode;
use Misuzu\Parsers\ParserInterface;
class BBCodeParser implements ParserInterface {
private static $instance;
public static function instance(): BBCodeParser {
return is_null(static::$instance) ? (static::$instance = new BBCodeParser()) : static::$instance;
}
private $tags = [];
public function __construct(array $tags = []) {

44
src/Parsers/Parser.php Normal file
View file

@ -0,0 +1,44 @@
<?php
namespace Misuzu\Parsers;
use InvalidArgumentException;
use Misuzu\Parsers\BBCode\BBCodeParser;
final class Parser {
public const PLAIN = 0;
public const BBCODE = 1;
public const MARKDOWN = 2;
private const PARSERS = [
self::PLAIN => PlainParser::class,
self::BBCODE => BBCodeParser::class,
self::MARKDOWN => MarkdownParser::class,
];
public const NAMES = [
self::PLAIN => 'Plain text',
self::BBCODE => 'BB Code',
self::MARKDOWN => 'Markdown',
];
private static $instances = [];
public static function isValid(int $parser): bool {
return array_key_exists($parser, self::PARSERS);
}
public static function name(int $parser): string {
return self::isValid($parser) ? self::NAMES[$parser] : '';
}
public static function instance(int $parser): ParserInterface {
if(!self::isValid($parser))
throw new InvalidArgumentException('Invalid parser.');
if(!isset(self::$instances[$parser])) {
$className = self::PARSERS[$parser];
self::$instances[$parser] = new $className;
}
return self::$instances[$parser];
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace Misuzu\Parsers;
class PlainParser implements ParserInterface {
public function parseText(string $text): string {
return nl2br($text);
}
public function parseLine(string $line): string {
return $line;
}
}

View file

@ -1,60 +0,0 @@
<?php
use Misuzu\Parsers\MarkdownParser;
use Misuzu\Parsers\BBCode\BBCodeParser;
define('MSZ_PARSER_PLAIN', 0);
define('MSZ_PARSER_BBCODE', 1);
define('MSZ_PARSER_MARKDOWN', 2);
define('MSZ_PARSERS', [
MSZ_PARSER_PLAIN,
MSZ_PARSER_BBCODE,
MSZ_PARSER_MARKDOWN,
]);
define('MSZ_PARSERS_NAMES', [
MSZ_PARSER_PLAIN => 'Plain text',
MSZ_PARSER_BBCODE => 'BB Code',
MSZ_PARSER_MARKDOWN => 'Markdown',
]);
function parser_is_valid(int $parser): bool {
return in_array($parser, MSZ_PARSERS, true);
}
function parser_name(int $parser): string {
return parser_is_valid($parser) ? MSZ_PARSERS_NAMES[$parser] : '';
}
function parse_text(string $text, int $parser): string {
if(!parser_is_valid($parser)) {
return '';
}
switch($parser) {
case MSZ_PARSER_MARKDOWN:
return MarkdownParser::instance()->parseText($text);
case MSZ_PARSER_BBCODE:
return BBCodeParser::instance()->parseText($text);
case MSZ_PARSER_PLAIN:
return nl2br($text);
}
}
function parse_line(string $line, int $parser): string {
if(!parser_is_valid($parser)) {
return '';
}
switch($parser) {
case MSZ_PARSER_MARKDOWN:
return MarkdownParser::instance()->parseLine($line);
case MSZ_PARSER_BBCODE:
return BBCodeParser::instance()->parseLine($line);
case MSZ_PARSER_PLAIN:
return $line;
}
}

View file

@ -4,6 +4,7 @@ namespace Misuzu;
use Twig_Extension;
use Twig_Filter;
use Twig_Function;
use Misuzu\Parsers\Parser;
final class TwigMisuzu extends Twig_Extension {
public function getFilters() {
@ -13,19 +14,13 @@ final class TwigMisuzu extends Twig_Extension {
new Twig_Filter('first_paragraph', 'first_paragraph'),
new Twig_Filter('byte_symbol', 'byte_symbol'),
new Twig_Filter('html_link', 'html_link'),
new Twig_Filter('parse_line', 'parse_line'),
new Twig_Filter('parse_text', 'parse_text'),
// deprecate this call, convert to html in php
new Twig_Filter('parse_text', fn(string $text, int $parser): string => Parser::instance($parser)->parseText($text)),
new Twig_Filter('asset_url', [static::class, 'assetUrl']),
new Twig_Filter('perms_check', 'perms_check'),
new Twig_Filter('bg_settings', 'user_background_settings_strings'),
new Twig_Filter('colour_contrast', 'colour_get_css_contrast'),
new Twig_Filter('colour_props', 'colour_get_properties'),
new Twig_Filter('colour_hex', 'colour_get_hex'),
new Twig_Filter('colour_inherit', 'colour_get_inherit'),
new Twig_Filter('clamp', 'clamp'),
new Twig_Filter('log_format', function (string $text, string $json): string {
return vsprintf($text, json_decode($json));
}),
new Twig_Filter('log_format', fn(string $text, string $json) => vsprintf($text, json_decode($json))),
];
}

View file

@ -9,7 +9,7 @@ function user_login_attempt_record(bool $success, ?int $userId, string $ipAddres
$storeAttempt->bind('attempt_success', $success ? 1 : 0);
$storeAttempt->bind('attempt_ip', $ipAddress);
$storeAttempt->bind('attempt_country', ip_country_code($ipAddress));
$storeAttempt->bind('attempt_country', \Misuzu\Net\IPAddress::country($ipAddress));
$storeAttempt->bind('attempt_user_agent', $userAgent);
$storeAttempt->bind('user_id', $userId, $userId === null ? PDO::PARAM_NULL : PDO::PARAM_INT);
$storeAttempt->execute();

View file

@ -2,6 +2,7 @@
namespace Misuzu\Users;
use Misuzu\DB;
use Misuzu\Net\IPAddress;
class User {
private const USER_SELECT = '
@ -42,7 +43,7 @@ class User {
') ->bind('username', $username)->bind('email', $email)
->bind('register_ip', $ipAddress)->bind('last_ip', $ipAddress)
->bind('password', user_password_hash($password))
->bind('user_country', ip_country_code($ipAddress))
->bind('user_country', IPAddress::country($ipAddress))
->executeGetId();
if($createUser < 1)

View file

@ -22,7 +22,7 @@ function user_session_create(
');
$createSession->bind('user_id', $userId);
$createSession->bind('session_ip', $ipAddress);
$createSession->bind('session_country', ip_country_code($ipAddress));
$createSession->bind('session_country', \Misuzu\Net\IPAddress::country($ipAddress));
$createSession->bind('session_user_agent', $userAgent);
$createSession->bind('session_key', $sessionKey);
@ -123,7 +123,7 @@ function user_session_bump_active(int $sessionId, string $ipAddress = null): voi
WHERE `session_id` = :session_id
');
$bump->bind('session_id', $sessionId);
$bump->bind('last_ip', $ipAddress ?? ip_remote_address());
$bump->bind('last_ip', $ipAddress ?? \Misuzu\Net\IPAddress::remote());
$bump->execute();
}

View file

@ -171,7 +171,7 @@ function user_bump_last_active(int $userId, string $ipAddress = null): void {
`last_ip` = INET6_ATON(:last_ip)
WHERE `user_id` = :user_id
');
$bumpUserLast->bind('last_ip', $ipAddress ?? ip_remote_address());
$bumpUserLast->bind('last_ip', $ipAddress ?? \Misuzu\Net\IPAddress::remote());
$bumpUserLast->bind('user_id', $userId);
$bumpUserLast->execute();
}
@ -306,12 +306,12 @@ define('MSZ_E_USER_ABOUT_INVALID_PARSER', 2);
define('MSZ_E_USER_ABOUT_TOO_LONG', 3);
define('MSZ_E_USER_ABOUT_UPDATE_FAILED', 4);
function user_set_about_page(int $userId, string $content, int $parser = MSZ_PARSER_PLAIN): int {
function user_set_about_page(int $userId, string $content, int $parser = \Misuzu\Parsers\Parser::PLAIN): int {
if($userId < 1) {
return MSZ_E_USER_ABOUT_INVALID_USER;
}
if(!parser_is_valid($parser)) {
if(!\Misuzu\Parsers\Parser::isValid($parser)) {
return MSZ_E_USER_ABOUT_INVALID_PARSER;
}
@ -344,12 +344,12 @@ define('MSZ_E_USER_SIGNATURE_INVALID_PARSER', 2);
define('MSZ_E_USER_SIGNATURE_TOO_LONG', 3);
define('MSZ_E_USER_SIGNATURE_UPDATE_FAILED', 4);
function user_set_signature(int $userId, string $content, int $parser = MSZ_PARSER_PLAIN): int {
function user_set_signature(int $userId, string $content, int $parser = \Misuzu\Parsers\Parser::PLAIN): int {
if($userId < 1) {
return MSZ_E_USER_SIGNATURE_INVALID_USER;
}
if(!parser_is_valid($parser)) {
if(!\Misuzu\Parsers\Parser::isValid($parser)) {
return MSZ_E_USER_SIGNATURE_INVALID_PARSER;
}

View file

@ -76,7 +76,7 @@ function audit_log(
array $params = [],
?string $ipAddress = null
): void {
$ipAddress = $ipAddress ?? ip_remote_address();
$ipAddress = $ipAddress ?? \Misuzu\Net\IPAddress::remote();
for($i = 0; $i < count($params); $i++) {
if(preg_match('#^(-?[0-9]+)$#', $params[$i])) {
@ -94,7 +94,7 @@ function audit_log(
$addLog->bind('user', $userId < 1 ? null : $userId);
$addLog->bind('params', json_encode($params));
$addLog->bind('ip', $ipAddress);
$addLog->bind('country', ip_country_code($ipAddress));
$addLog->bind('country', \Misuzu\Net\IPAddress::country($ipAddress));
$addLog->execute();
}

View file

@ -1,136 +0,0 @@
<?php
define('MSZ_COLOUR_INHERIT', 0x40000000);
define('MSZ_COLOUR_READABILITY_THRESHOLD', 186);
define('MSZ_COLOUR_LUMINANCE_WEIGHT_RED', 0.299);
define('MSZ_COLOUR_LUMINANCE_WEIGHT_GREEN', 0.587);
define('MSZ_COLOUR_LUMINANCE_WEIGHT_BLUE', 0.114);
function colour_create(): int {
return 0;
}
function colour_none(): int {
return MSZ_COLOUR_INHERIT;
}
function colour_set_inherit(int &$colour, bool $enabled = true): void {
if($enabled) {
$colour |= MSZ_COLOUR_INHERIT;
} else {
$colour &= ~MSZ_COLOUR_INHERIT;
}
}
function colour_get_inherit(int $colour): bool {
return ($colour & MSZ_COLOUR_INHERIT) > 0;
}
function colour_get_red(int $colour): int {
return ($colour >> 16) & 0xFF;
}
function colour_set_red(int &$colour, int $red): void {
$red = $red & 0xFF;
$colour &= ~0xFF0000;
$colour |= $red << 16;
}
function colour_get_green(int $colour): int {
return ($colour >> 8) & 0xFF;
}
function colour_set_green(int &$colour, int $green): void {
$green = $green & 0xFF;
$colour &= ~0xFF00;
$colour |= $green << 8;
}
function colour_get_blue(int $colour): int {
return $colour & 0xFF;
}
function colour_set_blue(int &$colour, int $blue): void {
$blue = $blue & 0xFF;
$colour &= ~0xFF;
$colour |= $blue;
}
function colour_get_luminance(int $colour): int {
return MSZ_COLOUR_LUMINANCE_WEIGHT_RED * colour_get_red($colour)
+ MSZ_COLOUR_LUMINANCE_WEIGHT_GREEN * colour_get_green($colour)
+ MSZ_COLOUR_LUMINANCE_WEIGHT_BLUE * colour_get_blue($colour);
}
function colour_get_hex(int $colour, string $format = '#%s'): string {
return sprintf(
$format,
str_pad(dechex($colour & 0xFFFFFF), 6, '0', STR_PAD_LEFT)
);
}
function colour_get_css(int $colour): string {
if(colour_get_inherit($colour)) {
return 'inherit';
}
return colour_get_hex($colour);
}
function colour_get_css_contrast(
int $colour,
string $dark = 'dark',
string $light = 'light',
bool $inheritIsDark = true
): string {
if(colour_get_inherit($colour)) {
return $inheritIsDark ? $dark : $light;
}
return colour_get_luminance($colour) > MSZ_COLOUR_READABILITY_THRESHOLD
? $dark
: $light;
}
function colour_from_rgb(int &$colour, int $red, int $green, int $blue): bool {
colour_set_red($colour, $red);
colour_set_green($colour, $green);
colour_set_blue($colour, $blue);
return true;
}
function colour_from_hex(int &$colour, string $hex): bool {
if($hex[0] === '#') {
$hex = mb_substr($hex, 1);
}
$length = mb_strlen($hex);
if($length === 3) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
} elseif($length !== 6) {
return false;
}
if(!ctype_xdigit($hex)) {
return false;
}
colour_from_rgb(
$colour,
hexdec(mb_substr($hex, 0, 2)),
hexdec(mb_substr($hex, 2, 2)),
hexdec(mb_substr($hex, 4, 2))
);
return true;
}
function colour_get_properties(int $colour): array {
return [
'red' => colour_get_red($colour),
'green' => colour_get_green($colour),
'blue' => colour_get_blue($colour),
'inherit' => colour_get_inherit($colour),
'luminance' => colour_get_luminance($colour),
];
}

View file

@ -325,98 +325,3 @@ function news_post_get(int $postId): array {
$getPost->bind(':post_id', $postId);
return $getPost->fetch();
}
define('MSZ_NEWS_FEED_ATOM', 'atom');
define('MSZ_NEWS_FEED_RSS', 'rss');
define('MSZ_NEWS_SUPPORTED_FEEDS', [
MSZ_NEWS_FEED_ATOM, MSZ_NEWS_FEED_RSS,
]);
function news_feed_supported(string $type): string {
return in_array($type, MSZ_NEWS_SUPPORTED_FEEDS);
}
function news_feed(string $type, array $posts, array $info): string {
if(!news_feed_supported($type)) {
return '';
}
$document = new DOMDocument('1.0', 'utf-8');
$urlPrefix = url_prefix(false);
$htmlUrl = $urlPrefix . $info['html-url'];
$feedUrl = $urlPrefix . $info['feed-url'];
if($type === 'rss') {
$dateFormat = 'r';
$rss = $document->appendChild($document->createElement('rss'));
$rss->setAttribute('version', '2.0');
$rss->setAttribute('xmlns:atom', 'http://www.w3.org/2005/Atom');
$feed = $rss->appendChild($document->createElement('channel'));
$feed->appendChild($document->createElement('ttl', '900'));
} else {
$dateFormat = 'c';
$feed = $document->appendChild($document->createElement('feed'));
$feed->setAttribute('xmlns', 'http://www.w3.org/2005/Atom');
$link = $feed->appendChild($document->createElement('link'));
$link->setAttribute('href', $htmlUrl);
}
$feed->appendChild($document->createElement('title', $info['title']));
$feed->appendChild($document->createElement(
$type === 'rss' ? 'description' : 'subtitle', $info['subtitle']
));
$feed->appendChild($document->createElement(
$type === 'rss' ? 'link' : 'id', $htmlUrl
));
$feed->appendChild($document->createElement(
$type === 'rss' ? 'pubDate' : 'updated',
date($dateFormat, strtotime($posts[0]['post_created']))
));
$link = $feed->appendChild($document->createElement($type === 'rss' ? 'atom:link' : 'link'));
$link->setAttribute('rel', 'self');
$link->setAttribute('href', $feedUrl);
foreach($posts as $post) {
$entry = $feed->appendChild($document->createElement($type === 'rss' ? 'item' : 'entry'));
$entry->appendChild($document->createElement('title', $post['post_title']));
$entry->appendChild($document->createElement(
$type === 'rss' ? 'link' : 'id',
$urlPrefix . url('news-post', ['post' => $post['post_id']])
));
$entry->appendChild($document->createElement(
$type === 'rss' ? 'pubDate' : 'updated',
date($dateFormat, strtotime($post['post_created']))
));
$entry->appendChild($document->createElement(
$type === 'rss' ? 'description' : 'summary',
first_paragraph($post['post_text'])
));
if($type === 'rss') {
$entry->appendChild($document->createElement(
'comments',
$urlPrefix . url('news-post-comments', ['post' => $post['post_id']])
));
$guid = $entry->appendChild($document->createElement(
'guid',
$urlPrefix . url('news-post', ['post' => $post['post_id']])
));
$guid->setAttribute('isPermaLink', 'true');
} else {
$link = $entry->appendChild($document->createElement('link'));
$link->setAttribute('href', $urlPrefix . url('news-post', ['post' => $post['post_id']]));
$link->setAttribute('type', 'text/html');
$html = $entry->appendChild($document->createElement(
'content',
htmlentities(parse_text($post['post_text'], MSZ_PARSER_MARKDOWN))
));
$html->setAttribute('type', 'html');
$author = $entry->appendChild($document->createElement('author'));
$author->appendChild($document->createElement('name', $post['username']));
$author->appendChild($document->createElement('uri', $urlPrefix . url('user-profile', ['user' => $post['user_id']])));
}
}
return $document->saveXML();
}

View file

@ -76,7 +76,7 @@
<h1>{{ title }}</h1>
{% if change.change_text|length >= 1 %}
{{ change.change_text|parse_text(constant('MSZ_PARSER_MARKDOWN'))|raw }}
{{ change.change_text|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
{% else %}
<p>This change has no additional notes.</p>
{% endif %}

View file

@ -440,7 +440,7 @@
</a>
</div>
<div class="forum__post__text{% if post.post_parse == constant('MSZ_PARSER_MARKDOWN') %} markdown{% endif %}">
<div class="forum__post__text{% if post.post_parse == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}">
{{ post.post_text|escape|parse_text(post.post_parse)|raw }}
</div>
@ -464,7 +464,7 @@
{% endif %}
{% if post.post_display_signature and post.poster_signature_content|length > 0 %}
<div class="forum__post__signature{% if post.poster_signature_parser == constant('MSZ_PARSER_MARKDOWN') %} markdown{% endif %}">
<div class="forum__post__signature{% if post.poster_signature_parser == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}">
{{ post.poster_signature_content|escape|parse_text(post.poster_signature_parser)|raw }}
</div>
{% endif %}

View file

@ -139,8 +139,8 @@
<div class="forum__post__settings">
{{ input_select(
'post[parser]',
constant('MSZ_PARSERS_NAMES'),
posting_defaults.parser|default(posting_post.post_parse|default(posting_info.user_post_parse|default(constant('MSZ_PARSER_BBCODE')))),
constant('\\Misuzu\\Parsers\\Parser::NAMES'),
posting_defaults.parser|default(posting_post.post_parse|default(posting_info.user_post_parse|default(constant('\\Misuzu\\Parsers\\Parser::BBCODE')))),
null, null, false, 'forum__post__dropdown js-forum-posting-parser'
) }}
{% if is_opening and posting_types|length > 1 %}

View file

@ -8,7 +8,7 @@
{{ container_title(document.title) }}
<div class="container__content markdown">
{{ document.content|parse_text(constant('MSZ_PARSER_MARKDOWN'))|raw }}
{{ document.content|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
</div>
</div>
{% endblock %}

View file

@ -43,33 +43,31 @@
<div class="container">
{{ container_title('Colour') }}
{% set colour_props = edit_role.role_colour|default(constant('MSZ_COLOUR_INHERIT'))|colour_props %}
<label class="form__label">
<div class="form__label__text">Inherit Colour</div>
<div class="form__label__input">
{{ input_checkbox('role[colour][inherit]', '', colour_props.inherit) }}
{{ input_checkbox('role[colour][inherit]', '', role_colour is defined ? role_colour.inherit : true) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Red</div>
<div class="form__label__input">
{{ input_text('role[colour][red]', '', colour_props.red, 'number', '', false, {'min':0,'max':255}) }}
{{ input_text('role[colour][red]', '', role_colour.red|default(0), 'number', '', false, {'min':0,'max':255}) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Green</div>
<div class="form__label__input">
{{ input_text('role[colour][green]', '', colour_props.green, 'number', '', false, {'min':0,'max':255}) }}
{{ input_text('role[colour][green]', '', role_colour.green|default(0), 'number', '', false, {'min':0,'max':255}) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Blue</div>
<div class="form__label__input">
{{ input_text('role[colour][blue]', '', colour_props.blue, 'number', '', false, {'min':0,'max':255}) }}
{{ input_text('role[colour][blue]', '', role_colour.blue|default(0), 'number', '', false, {'min':0,'max':255}) }}
</div>
</label>

View file

@ -99,11 +99,11 @@
<label class="form__label">
<div class="form__label__text">Custom Colour</div>
<div class="form__label__input">
{{ input_checkbox('colour[enable]', '', manage_user.user_colour is not null and not manage_user.user_colour|colour_inherit, '', '', false, null, not can_edit_user) }}
{{ input_checkbox('colour[enable]', '', not user_colour.inherit, '', '', false, null, not can_edit_user) }}
</div>
</label>
{{ input_colour(can_edit_user ? 'colour[hex]' : '', '', manage_user.user_colour|default(0)|colour_hex) }}
{{ input_colour(can_edit_user ? 'colour[hex]' : '', '', '#%s'|format(user_colour.hex)) }}
</div>
{# TODO: if the hierarchy of the current user is too low to touch the role then opacity should be lowered and input disabled #}

View file

@ -33,7 +33,7 @@
<div class="news__preview__content markdown">
<h1>{{ post.post_title }}</h1>
<div class="news__preview__text">
{{ post.post_text|first_paragraph|parse_text(constant('MSZ_PARSER_MARKDOWN'))|raw }}
{{ post.post_text|first_paragraph|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
</div>
<div class="news__preview__links">
<a href="{{ url('news-post', {'post': post.post_id}) }}" class="news__preview__link">Continue reading</a>
@ -88,7 +88,7 @@
<div class="news__post__text markdown">
<h1>{{ post.post_title }}</h1>
{{ post.post_text|parse_text(constant('MSZ_PARSER_MARKDOWN'))|raw }}
{{ post.post_text|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
</div>
</div>
{% endmacro %}

View file

@ -181,11 +181,11 @@
{% if profile_is_editing %}
<div class="profile__signature__editor">
{{ input_select('about[parser]', constant('MSZ_PARSERS_NAMES'), profile_user.user_about_parser, '', '', false, 'profile__about__select') }}
{{ input_select('about[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), profile_user.user_about_parser, '', '', false, 'profile__about__select') }}
<textarea name="about[text]" class="input__textarea profile__about__text" id="about-textarea">{{ profile_user.user_about_content|escape }}</textarea>
</div>
{% else %}
<div class="profile__about__content{% if profile_is_editing %} profile__about__content--edit{% elseif profile_user.user_about_parser == constant('MSZ_PARSER_MARKDOWN') %} markdown{% endif %}">
<div class="profile__about__content{% if profile_is_editing %} profile__about__content--edit{% elseif profile_user.user_about_parser == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}">
{{ profile_user.user_about_content|escape|parse_text(profile_user.user_about_parser)|raw }}
</div>
{% endif %}
@ -198,11 +198,11 @@
{% if profile_is_editing %}
<div class="profile__signature__editor">
{{ input_select('signature[parser]', constant('MSZ_PARSERS_NAMES'), profile_user.user_signature_parser, '', '', false, 'profile__signature__select') }}
{{ input_select('signature[parser]', constant('\\Misuzu\\Parsers\\Parser::NAMES'), profile_user.user_signature_parser, '', '', false, 'profile__signature__select') }}
<textarea name="signature[text]" class="input__textarea profile__signature__text" id="signature-textarea">{{ profile_user.user_signature_content|escape }}</textarea>
</div>
{% else %}
<div class="profile__signature__content{% if profile_is_editing %} profile__signature__content--edit{% elseif profile_user.user_signature_parser == constant('MSZ_PARSER_MARKDOWN') %} markdown{% endif %}">
<div class="profile__signature__content{% if profile_is_editing %} profile__signature__content--edit{% elseif profile_user.user_signature_parser == constant('\\Misuzu\\Parsers\\Parser::MARKDOWN') %} markdown{% endif %}">
{{ profile_user.user_signature_content|escape|parse_text(profile_user.user_signature_parser)|raw }}
</div>
{% endif %}

View file

@ -245,7 +245,7 @@ function html_link(string $url, ?string $content = null, $attributes = []): stri
}
function html_colour(?int $colour, $attribs = '--user-colour'): string {
$colour = $colour ?? colour_none();
$colour = $colour == null ? \Misuzu\Colour::none() : new \Misuzu\Colour($colour);
if(is_string($attribs)) {
$attribs = [
@ -261,7 +261,7 @@ function html_colour(?int $colour, $attribs = '--user-colour'): string {
}
$css = '';
$value = colour_get_css($colour);
$value = $colour->getCSS();
foreach($attribs as $name => $format) {
$css .= $name . ':' . sprintf($format, $value) . ';';