diff --git a/misuzu.php b/misuzu.php index 53662df6..3555ffa6 100644 --- a/misuzu.php +++ b/misuzu.php @@ -1,8 +1,11 @@ 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'); diff --git a/public/auth/login.php b/public/auth/login.php index 798e2d89..71e1fdfe 100644 --- a/public/auth/login.php +++ b/public/auth/login.php @@ -1,6 +1,7 @@ 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) { diff --git a/public/auth/register.php b/public/auth/register.php index 3ca9c6ef..04d15b77 100644 --- a/public/auth/register.php +++ b/public/auth/register.php @@ -1,6 +1,8 @@ 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.'; } diff --git a/public/manage/general/blacklist.php b/public/manage/general/blacklist.php index b78d1c99..51b9df02 100644 --- a/public/manage/general/blacklist.php +++ b/public/manage/general/blacklist.php @@ -1,6 +1,8 @@ $notices, - 'blacklist' => ip_blacklist_list(), + 'blacklist' => IPAddressBlacklist::list(), ]); diff --git a/public/manage/users/role.php b/public/manage/users/role.php index 9abb1bf0..564f043e 100644 --- a/public/manage/users/role.php +++ b/public/manage/users/role.php @@ -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', [ diff --git a/public/manage/users/user.php b/public/manage/users/user.php index 76a40bb4..256927c5 100644 --- a/public/manage/users/user.php +++ b/public/manage/users/user.php @@ -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, diff --git a/public/manage/users/warnings.php b/public/manage/users/warnings.php index a330a1c9..53384c29 100644 --- a/public/manage/users/warnings.php +++ b/public/manage/users/warnings.php @@ -1,6 +1,8 @@ $categoryId]); } diff --git a/public/news/feed.php b/public/news/feed.php index 9a75ef7a..9541a718 100644 --- a/public/news/feed.php +++ b/public/news/feed.php @@ -1,11 +1,26 @@ 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); diff --git a/public/profile.php b/public/profile.php index 83b3e24b..e77c844d 100644 --- a/public/profile.php +++ b/public/profile.php @@ -1,6 +1,7 @@ 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) { diff --git a/src/CSRF.php b/src/CSRF.php index 566de84f..0a96ba37 100644 --- a/src/CSRF.php +++ b/src/CSRF.php @@ -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 { diff --git a/src/Colour.php b/src/Colour.php new file mode 100644 index 00000000..ae05a1f4 --- /dev/null +++ b/src/Colour.php @@ -0,0 +1,144 @@ +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; + } +} diff --git a/src/Debug/Stopwatch.php b/src/Debug/Stopwatch.php new file mode 100644 index 00000000..22580101 --- /dev/null +++ b/src/Debug/Stopwatch.php @@ -0,0 +1,54 @@ +{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; + } +} diff --git a/src/Feeds/AtomFeedSerializer.php b/src/Feeds/AtomFeedSerializer.php new file mode 100644 index 00000000..486017ad --- /dev/null +++ b/src/Feeds/AtomFeedSerializer.php @@ -0,0 +1,115 @@ +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; + } +} diff --git a/src/Feeds/Feed.php b/src/Feeds/Feed.php new file mode 100644 index 00000000..d4aa88b9 --- /dev/null +++ b/src/Feeds/Feed.php @@ -0,0 +1,78 @@ +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; + } +} diff --git a/src/Feeds/FeedItem.php b/src/Feeds/FeedItem.php new file mode 100644 index 00000000..34d8e6d5 --- /dev/null +++ b/src/Feeds/FeedItem.php @@ -0,0 +1,110 @@ +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; + } +} diff --git a/src/Feeds/FeedSerializer.php b/src/Feeds/FeedSerializer.php new file mode 100644 index 00000000..6595b762 --- /dev/null +++ b/src/Feeds/FeedSerializer.php @@ -0,0 +1,6 @@ +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; + } +} diff --git a/src/Feeds/XmlFeedSerializer.php b/src/Feeds/XmlFeedSerializer.php new file mode 100644 index 00000000..289e0b3e --- /dev/null +++ b/src/Feeds/XmlFeedSerializer.php @@ -0,0 +1,79 @@ +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; +} diff --git a/src/Forum/forum.php b/src/Forum/forum.php index 5c1ea189..ca6a878b 100644 --- a/src/Forum/forum.php +++ b/src/Forum/forum.php @@ -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 { diff --git a/src/Forum/post.php b/src/Forum/post.php index 9013c95f..c569eb50 100644 --- a/src/Forum/post.php +++ b/src/Forum/post.php @@ -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 { diff --git a/src/Net/GeoIP.php b/src/Net/GeoIP.php new file mode 100644 index 00000000..6cdb5728 --- /dev/null +++ b/src/Net/GeoIP.php @@ -0,0 +1,23 @@ +country($ipAddress); + } +} diff --git a/src/Net/IPAddress.php b/src/Net/IPAddress.php new file mode 100644 index 00000000..bbb039be --- /dev/null +++ b/src/Net/IPAddress.php @@ -0,0 +1,108 @@ + 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; + } +} diff --git a/src/Net/IPAddressBlacklist.php b/src/Net/IPAddressBlacklist.php new file mode 100644 index 00000000..8ccd2b97 --- /dev/null +++ b/src/Net/IPAddressBlacklist.php @@ -0,0 +1,80 @@ + 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(); + } +} diff --git a/src/Net/geoip.php b/src/Net/geoip.php deleted file mode 100644 index 07f1e01a..00000000 --- a/src/Net/geoip.php +++ /dev/null @@ -1,38 +0,0 @@ -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); - }); -} diff --git a/src/Net/ip.php b/src/Net/ip.php deleted file mode 100644 index 07a9c30f..00000000 --- a/src/Net/ip.php +++ /dev/null @@ -1,184 +0,0 @@ - 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(); -} diff --git a/src/Parsers/BBCode/BBCodeParser.php b/src/Parsers/BBCode/BBCodeParser.php index 594d9a15..ceeb7845 100644 --- a/src/Parsers/BBCode/BBCodeParser.php +++ b/src/Parsers/BBCode/BBCodeParser.php @@ -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 = []) { diff --git a/src/Parsers/Parser.php b/src/Parsers/Parser.php new file mode 100644 index 00000000..f49cffb9 --- /dev/null +++ b/src/Parsers/Parser.php @@ -0,0 +1,44 @@ + 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]; + } +} diff --git a/src/Parsers/PlainParser.php b/src/Parsers/PlainParser.php new file mode 100644 index 00000000..8cec778a --- /dev/null +++ b/src/Parsers/PlainParser.php @@ -0,0 +1,12 @@ + '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; - } -} diff --git a/src/TwigMisuzu.php b/src/TwigMisuzu.php index bfa29e17..23d96291 100644 --- a/src/TwigMisuzu.php +++ b/src/TwigMisuzu.php @@ -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))), ]; } diff --git a/src/Users/login_attempt.php b/src/Users/login_attempt.php index a45f2cc9..3e594a75 100644 --- a/src/Users/login_attempt.php +++ b/src/Users/login_attempt.php @@ -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(); diff --git a/src/Users/object.php b/src/Users/object.php index c516805b..8498dc9a 100644 --- a/src/Users/object.php +++ b/src/Users/object.php @@ -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) diff --git a/src/Users/session.php b/src/Users/session.php index 39e676ea..e50748a1 100644 --- a/src/Users/session.php +++ b/src/Users/session.php @@ -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(); } diff --git a/src/Users/user.php b/src/Users/user.php index 3c4169df..fbb04cf8 100644 --- a/src/Users/user.php +++ b/src/Users/user.php @@ -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; } diff --git a/src/audit_log.php b/src/audit_log.php index f874d85c..3a762a91 100644 --- a/src/audit_log.php +++ b/src/audit_log.php @@ -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(); } diff --git a/src/colour.php b/src/colour.php deleted file mode 100644 index 55103046..00000000 --- a/src/colour.php +++ /dev/null @@ -1,136 +0,0 @@ - 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), - ]; -} diff --git a/src/news.php b/src/news.php index a15760da..7ace75b9 100644 --- a/src/news.php +++ b/src/news.php @@ -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(); -} diff --git a/templates/changelog/change.twig b/templates/changelog/change.twig index 3348c05e..85e73e4f 100644 --- a/templates/changelog/change.twig +++ b/templates/changelog/change.twig @@ -76,7 +76,7 @@

{{ title }}

{% 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 %}

This change has no additional notes.

{% endif %} diff --git a/templates/forum/macros.twig b/templates/forum/macros.twig index 6f609d65..8f80e220 100644 --- a/templates/forum/macros.twig +++ b/templates/forum/macros.twig @@ -440,7 +440,7 @@ -
+
{{ post.post_text|escape|parse_text(post.post_parse)|raw }}
@@ -464,7 +464,7 @@ {% endif %} {% if post.post_display_signature and post.poster_signature_content|length > 0 %} -
+
{{ post.poster_signature_content|escape|parse_text(post.poster_signature_parser)|raw }}
{% endif %} diff --git a/templates/forum/posting.twig b/templates/forum/posting.twig index 425f55a5..653f18f6 100644 --- a/templates/forum/posting.twig +++ b/templates/forum/posting.twig @@ -139,8 +139,8 @@
{{ 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 %} diff --git a/templates/info/view.twig b/templates/info/view.twig index 4e8a81ff..8c46a905 100644 --- a/templates/info/view.twig +++ b/templates/info/view.twig @@ -8,7 +8,7 @@ {{ container_title(document.title) }}
- {{ document.content|parse_text(constant('MSZ_PARSER_MARKDOWN'))|raw }} + {{ document.content|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
{% endblock %} diff --git a/templates/manage/users/role.twig b/templates/manage/users/role.twig index 9cbc820c..eeb8fd54 100644 --- a/templates/manage/users/role.twig +++ b/templates/manage/users/role.twig @@ -43,33 +43,31 @@
{{ container_title('Colour') }} - {% set colour_props = edit_role.role_colour|default(constant('MSZ_COLOUR_INHERIT'))|colour_props %} - diff --git a/templates/manage/users/user.twig b/templates/manage/users/user.twig index 5a835b6b..e0ebb92f 100644 --- a/templates/manage/users/user.twig +++ b/templates/manage/users/user.twig @@ -99,11 +99,11 @@ - {{ 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)) }}
{# TODO: if the hierarchy of the current user is too low to touch the role then opacity should be lowered and input disabled #} diff --git a/templates/news/macros.twig b/templates/news/macros.twig index a7ca246a..864e31d9 100644 --- a/templates/news/macros.twig +++ b/templates/news/macros.twig @@ -33,7 +33,7 @@

{{ post.post_title }}

- {{ 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 }}
{% endmacro %} diff --git a/templates/profile/index.twig b/templates/profile/index.twig index 3d57ae39..807e6374 100644 --- a/templates/profile/index.twig +++ b/templates/profile/index.twig @@ -181,11 +181,11 @@ {% if profile_is_editing %}
- {{ 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') }}
{% else %} -
+
{{ profile_user.user_about_content|escape|parse_text(profile_user.user_about_parser)|raw }}
{% endif %} @@ -198,11 +198,11 @@ {% if profile_is_editing %}
- {{ 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') }}
{% else %} -
+
{{ profile_user.user_signature_content|escape|parse_text(profile_user.user_signature_parser)|raw }}
{% endif %} diff --git a/utility.php b/utility.php index d60e44e8..e2af3b5f 100644 --- a/utility.php +++ b/utility.php @@ -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) . ';';