Added sanity to changelog code.

This commit is contained in:
flash 2020-05-20 18:09:38 +00:00
parent 4cac71c23e
commit c365e32d89
34 changed files with 1010 additions and 646 deletions

View file

@ -6,6 +6,8 @@ use Misuzu\Database\Database;
use Misuzu\Database\DatabaseMigrationManager;
use Misuzu\Net\GeoIP;
use Misuzu\Net\IPAddress;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
define('MSZ_STARTUP', microtime(true));
define('MSZ_ROOT', __DIR__);
@ -29,6 +31,7 @@ set_include_path(get_include_path() . PATH_SEPARATOR . MSZ_ROOT);
set_exception_handler(function(\Throwable $ex) {
http_response_code(500);
ob_clean();
if(MSZ_CLI || MSZ_DEBUG) {
header('Content-Type: text/plain; charset=utf-8');
@ -63,7 +66,6 @@ class_alias(\Misuzu\Http\HttpRequestMessage::class, '\HttpRequest');
require_once 'utility.php';
require_once 'src/perms.php';
require_once 'src/audit_log.php';
require_once 'src/changelog.php';
require_once 'src/manage.php';
require_once 'src/url.php';
require_once 'src/Forum/perms.php';
@ -450,6 +452,10 @@ MIG;
user_session_stop(true);
$userDisplayInfo = [];
} else {
try {
User::byId($cookieData['user_id'])->setCurrent();
} catch(UserNotFoundException $ex) {}
user_bump_last_active($cookieData['user_id']);
user_session_bump_active(user_session_current('session_id'));

View file

@ -1,95 +1,2 @@
<?php
namespace Misuzu;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
require_once '../misuzu.php';
$changelogChange = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0;
$changelogDate = !empty($_GET['d']) && is_string($_GET['d']) ? (string)$_GET['d'] : '';
$changelogUser = !empty($_GET['u']) && is_string($_GET['u']) ? (int)$_GET['u'] : 0;
$changelogTags = !empty($_GET['t']) && is_string($_GET['t']) ? (string)$_GET['t'] : '';
if($changelogChange > 0) {
$change = changelog_change_get($changelogChange);
if(!$change) {
echo render_error(404);
return;
}
$commentsCategoryName = "changelog-date-{$change['change_date']}";
try {
$commentsCategory = CommentsCategory::byName($commentsCategoryName);
} catch(CommentsCategoryNotFoundException $ex) {
$commentsCategory = new CommentsCategory($commentsCategoryName);
$commentsCategory->save();
}
try {
$commentsUser = User::byId(user_session_current('user_id', 0));
} catch(UserNotFoundException $ex) {
$commentsUser = null;
}
Template::render('changelog.change', [
'change' => $change,
'tags' => changelog_change_tags_get($change['change_id']),
'comments_category' => $commentsCategory,
'comments_user' => $commentsUser,
]);
return;
}
if(!empty($changelogDate)) {
$dateParts = explode('-', $changelogDate, 3);
if(count($dateParts) !== 3
|| !array_test($dateParts, 'ctype_digit')
|| !checkdate($dateParts[1], $dateParts[2], $dateParts[0])) {
echo render_error(404);
return;
}
}
$changesCount = !empty($changelogDate) ? -1 : changelog_count_changes($changelogDate, $changelogUser);
$changesPagination = new Pagination($changesCount, 30);
$changes = $changesCount === -1 || $changesPagination->hasValidOffset()
? changelog_get_changes($changelogDate, $changelogUser, $changesPagination->getOffset(), $changesPagination->getRange())
: [];
if(!$changes) {
http_response_code(404);
}
if(!empty($changelogDate) && count($changes) > 0) {
$commentsCategoryName = "changelog-date-{$changelogDate}";
try {
$commentsCategory = CommentsCategory::byName($commentsCategoryName);
} catch(CommentsCategoryNotFoundException $ex) {
$commentsCategory = new CommentsCategory($commentsCategoryName);
$commentsCategory->save();
}
try {
$commentsUser = User::byId(user_session_current('user_id', 0));
} catch(UserNotFoundException $ex) {
$commentsUser = null;
}
Template::set([
'comments_category' => $commentsCategory,
'comments_user' => $commentsUser,
]);
}
Template::render('changelog.index', [
'changes' => $changes,
'changelog_count' => $changesCount,
'changelog_date' => $changelogDate,
'changelog_user' => $changelogUser,
'changelog_pagination' => $changesPagination,
]);
require_once __DIR__ . '/index.php';

View file

@ -29,9 +29,8 @@ if(!CSRF::validateRequest()) {
return;
}
try {
$currentUserInfo = User::byId(user_session_current('user_id', 0));
} catch(UserNotFoundException $ex) {
$currentUserInfo = User::getCurrent();
if($currentUserInfo === null) {
echo render_info_or_json($isXHR, 'You must be logged in to manage comments.', 401);
return;
}

View file

@ -22,6 +22,13 @@ Router::addRoutes(
Route::get('/info', 'index', 'Info'),
Route::get('/info/([A-Za-z0-9_/]+)', 'page', 'Info'),
// Changelog
Route::get('/changelog', 'index', 'Changelog')->addChildren(
Route::get('.atom', 'feedAtom'),
Route::get('.rss', 'feedRss'),
Route::get('/change/([0-9]+)', 'change'),
),
// News
Route::get('/news', 'index', 'News')->addChildren(
Route::get('.atom', 'feedIndexAtom'),
@ -52,6 +59,7 @@ Router::addRoutes(
Route::get('/index.php', url('index')),
Route::get('/info.php', url('info')),
Route::get('/settings.php', url('settings-index')),
Route::get('/changelog.php', 'legacy', 'Changelog'),
Route::get('/info.php/([A-Za-z0-9_/]+)', 'redir', 'Info'),
Route::get('/auth.php', 'legacy', 'Auth'),
Route::get('/news.php', 'legacy', 'News'),

View file

@ -1,6 +1,13 @@
<?php
namespace Misuzu;
use Misuzu\Changelog\ChangelogChange;
use Misuzu\Changelog\ChangelogChangeNotFoundException;
use Misuzu\Changelog\ChangelogTag;
use Misuzu\Changelog\ChangelogTagNotFoundException;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
require_once '../../../misuzu.php';
if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) {
@ -8,122 +15,75 @@ if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_P
return;
}
$changeId = (int)($_GET['c'] ?? 0);
define('MANAGE_ACTIONS', [
['action_id' => ChangelogChange::ACTION_ADD, 'action_name' => 'Added'],
['action_id' => ChangelogChange::ACTION_REMOVE, 'action_name' => 'Removed'],
['action_id' => ChangelogChange::ACTION_UPDATE, 'action_name' => 'Updated'],
['action_id' => ChangelogChange::ACTION_FIX, 'action_name' => 'Fixed'],
['action_id' => ChangelogChange::ACTION_IMPORT, 'action_name' => 'Imported'],
['action_id' => ChangelogChange::ACTION_REVERT, 'action_name' => 'Reverted'],
]);
$changeId = (int)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
$tags = ChangelogTag::all();
if($changeId > 0)
try {
$change = ChangelogChange::byId($changeId);
} catch(ChangelogChangeNotFoundException $ex) {
url_redirect('manage-changelog-changes');
return;
}
if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if(!empty($_POST['change']) && is_array($_POST['change'])) {
if($changeId > 0) {
$postChange = DB::prepare('
UPDATE `msz_changelog_changes`
SET `change_log` = :log,
`change_text` = :text,
`change_action` = :action,
`user_id` = :user,
`change_created` = :created
WHERE `change_id` = :change_id
');
$postChange->bind('change_id', $changeId);
} else {
$postChange = DB::prepare('
INSERT INTO `msz_changelog_changes`
(
`change_log`, `change_text`, `change_action`,
`user_id`, `change_created`
)
VALUES
(:log, :text, :action, :user, :created)
');
if(!isset($change)) {
$change = new ChangelogChange;
$isNew = true;
}
$postChange->bind('log', $_POST['change']['log']);
$postChange->bind('action', $_POST['change']['action']);
$postChange->bind('text', strlen($_POST['change']['text'])
? $_POST['change']['text']
: null);
$postChange->bind('user', is_numeric($_POST['change']['user'])
? $_POST['change']['user']
: null);
$postChange->bind('created', strlen($_POST['change']['created'])
? $_POST['change']['created']
: null);
$postChange->execute();
$changeUserId = filter_var($_POST['change']['user'], FILTER_SANITIZE_NUMBER_INT);
if($changeUserId === 0)
$changeUser = null;
else
try {
$changeUser = User::byId($changeUserId);
} catch(UserNotFoundException $ex) {
$changeUser = User::getCurrent();
}
if($changeId < 1) {
$changeId = DB::lastId();
audit_log(MSZ_AUDIT_CHANGELOG_ENTRY_CREATE, user_session_current('user_id', 0), [$changeId]);
} else {
audit_log(MSZ_AUDIT_CHANGELOG_ENTRY_EDIT, user_session_current('user_id', 0), [$changeId]);
}
$change->setHeader($_POST['change']['log'])
->setBody($_POST['change']['text'])
->setAction($_POST['change']['action'])
->setUser($changeUser)
->save();
audit_log(
empty($isNew)
? MSZ_AUDIT_CHANGELOG_ENTRY_EDIT
: MSZ_AUDIT_CHANGELOG_ENTRY_CREATE,
User::getCurrent()->getId(),
[$change->getId()]
);
}
if(!empty($_POST['tags']) && is_array($_POST['tags']) && array_test($_POST['tags'], 'ctype_digit')) {
$setTags = array_apply($_POST['tags'], 'intval');
$removeTags = DB::prepare(sprintf('
DELETE FROM `msz_changelog_change_tags`
WHERE `change_id` = :change_id
AND `tag_id` NOT IN (%s)
', implode(',', $setTags)));
$removeTags->bind('change_id', $changeId);
$removeTags->execute();
$addTag = DB::prepare('
INSERT IGNORE INTO `msz_changelog_change_tags`
(`change_id`, `tag_id`)
VALUES
(:change_id, :tag_id)
');
$addTag->bind('change_id', $changeId);
foreach($setTags as $role) {
$addTag->bind('tag_id', $role);
$addTag->execute();
}
if(isset($change) && !empty($_POST['tags']) && is_array($_POST['tags']) && array_test($_POST['tags'], 'ctype_digit')) {
$applyTags = [];
foreach($_POST['tags'] as $tagId)
try {
$applyTags[] = ChangelogTag::byId((int)filter_var($tagId, FILTER_SANITIZE_NUMBER_INT));
} catch(ChangelogTagNotFoundException $ex) {}
$change->setTags($applyTags);
}
}
$actions = [
['action_id' => MSZ_CHANGELOG_ACTION_ADD, 'action_name' => 'Added'],
['action_id' => MSZ_CHANGELOG_ACTION_REMOVE, 'action_name' => 'Removed'],
['action_id' => MSZ_CHANGELOG_ACTION_UPDATE, 'action_name' => 'Updated'],
['action_id' => MSZ_CHANGELOG_ACTION_FIX, 'action_name' => 'Fixed'],
['action_id' => MSZ_CHANGELOG_ACTION_IMPORT, 'action_name' => 'Imported'],
['action_id' => MSZ_CHANGELOG_ACTION_REVERT, 'action_name' => 'Reverted'],
];
if($changeId > 0) {
$getChange = DB::prepare('
SELECT
`change_id`, `change_log`, `change_text`, `user_id`,
`change_action`, `change_created`
FROM `msz_changelog_changes`
WHERE `change_id` = :change_id
');
$getChange->bind('change_id', $changeId);
$change = $getChange->fetch();
if(!$change) {
url_redirect('manage-changelog-changes');
if(!empty($isNew)) {
url_redirect('manage-changelog-change', ['change' => $change->getId()]);
return;
}
}
$getChangeTags = DB::prepare('
SELECT
ct.`tag_id`, ct.`tag_name`,
(
SELECT COUNT(`change_id`) > 0
FROM `msz_changelog_change_tags`
WHERE `tag_id` = ct.`tag_id`
AND `change_id` = :change_id
) AS `has_tag`
FROM `msz_changelog_tags` AS ct
');
$getChangeTags->bind('change_id', $change['change_id'] ?? 0);
$changeTags = $getChangeTags->fetchAll();
Template::render('manage.changelog.change', [
'change' => $change ?? null,
'change_tags' => $changeTags,
'change_actions' => $actions,
'change_tags' => $tags,
'change_actions' => MANAGE_ACTIONS,
]);

View file

@ -1,6 +1,8 @@
<?php
namespace Misuzu;
use Misuzu\Changelog\ChangelogChange;
require_once '../../../misuzu.php';
if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) {
@ -8,54 +10,16 @@ if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_P
return;
}
$changesCount = (int)DB::query('
SELECT COUNT(`change_id`)
FROM `msz_changelog_changes`
')->fetchColumn();
$changelogPagination = new Pagination($changesCount, 30);
$changelogPagination = new Pagination(ChangelogChange::countAll(), 30);
if(!$changelogPagination->hasValidOffset()) {
echo render_error(404);
return;
}
$getChanges = DB::prepare('
SELECT
c.`change_id`, c.`change_log`, c.`change_created`, c.`change_action`,
u.`user_id`, u.`username`,
COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`,
DATE(`change_created`) AS `change_date`,
!ISNULL(c.`change_text`) AS `change_has_text`
FROM `msz_changelog_changes` AS c
LEFT JOIN `msz_users` AS u
ON u.`user_id` = c.`user_id`
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = u.`display_role`
ORDER BY c.`change_id` DESC
LIMIT :offset, :take
');
$getChanges->bind('take', $changelogPagination->getRange());
$getChanges->bind('offset', $changelogPagination->getOffset());
$changes = $getChanges->fetchAll();
$getTags = DB::prepare('
SELECT
t.`tag_id`, t.`tag_name`, t.`tag_description`
FROM `msz_changelog_change_tags` as ct
LEFT JOIN `msz_changelog_tags` as t
ON t.`tag_id` = ct.`tag_id`
WHERE ct.`change_id` = :change_id
');
// grab tags
for($i = 0; $i < count($changes); $i++) {
$getTags->bind('change_id', $changes[$i]['change_id']);
$changes[$i]['tags'] = $getTags->fetchAll();
}
$changes = ChangelogChange::all($changelogPagination);
Template::render('manage.changelog.changes', [
'changelog_changes' => $changes,
'changelog_changes_count' => $changesCount,
'changelog_pagination' => $changelogPagination,
]);

View file

@ -1,6 +1,10 @@
<?php
namespace Misuzu;
use Misuzu\Changelog\ChangelogTag;
use Misuzu\Changelog\ChangelogTagNotFoundException;
use Misuzu\Users\User;
require_once '../../../misuzu.php';
if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) {
@ -8,57 +12,41 @@ if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_P
return;
}
$tagId = (int)($_GET['t'] ?? 0);
$tagId = (int)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT);
if(!empty($_POST['tag']) && is_array($_POST['tag']) && CSRF::validateRequest()) {
if($tagId > 0) {
$updateTag = DB::prepare('
UPDATE `msz_changelog_tags`
SET `tag_name` = :name,
`tag_description` = :description,
`tag_archived` = :archived
WHERE `tag_id` = :id
');
$updateTag->bind('id', $tagId);
} else {
$updateTag = DB::prepare('
INSERT INTO `msz_changelog_tags`
(`tag_name`, `tag_description`, `tag_archived`)
VALUES
(:name, :description, :archived)
');
}
$updateTag->bind('name', $_POST['tag']['name']);
$updateTag->bind('description', $_POST['tag']['description']);
$updateTag->bind('archived', empty($_POST['tag']['archived']) ? null : date('Y-m-d H:i:s'));
$updateTag->execute();
if($tagId < 1) {
$tagId = DB::lastId();
audit_log(MSZ_AUDIT_CHANGELOG_TAG_EDIT, user_session_current('user_id', 0), [$tagId]);
url_redirect('manage-changelog-tag', ['tag' => $tagId]);
return;
} else {
audit_log(MSZ_AUDIT_CHANGELOG_TAG_CREATE, user_session_current('user_id', 0), [$tagId]);
}
}
if($tagId > 0) {
$getTag = DB::prepare('
SELECT `tag_id`, `tag_name`, `tag_description`, `tag_archived`, `tag_created`
FROM `msz_changelog_tags`
WHERE `tag_id` = :tag_id
');
$getTag->bind('tag_id', $tagId);
$tag = $getTag->fetch();
if($tag) {
Template::set('edit_tag', $tag);
} else {
if($tagId > 0)
try {
$tagInfo = ChangelogTag::byId($tagId);
} catch(ChangelogTagNotFoundException $ex) {
url_redirect('manage-changelog-tags');
return;
}
if(!empty($_POST['tag']) && is_array($_POST['tag']) && CSRF::validateRequest()) {
if(!isset($tagInfo)) {
$tagInfo = new ChangelogTag;
$isNew = true;
}
$tagInfo->setName($_POST['tag']['name'])
->setDescription($_POST['tag']['description'])
->setArchived(!empty($_POST['tag']['archived']))
->save();
audit_log(
empty($isNew)
? MSZ_AUDIT_CHANGELOG_TAG_EDIT
: MSZ_AUDIT_CHANGELOG_TAG_CREATE,
User::getCurrent()->getId(),
[$tagInfo->getId()]
);
if(!empty($isNew)) {
url_redirect('manage-changelog-tag', ['tag' => $tagInfo->getId()]);
return;
}
}
Template::render('manage.changelog.tag');
Template::render('manage.changelog.tag', [
'edit_tag' => $tagInfo ?? null,
]);

View file

@ -1,6 +1,8 @@
<?php
namespace Misuzu;
use Misuzu\Changelog\ChangelogTag;
require_once '../../../misuzu.php';
if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) {
@ -8,18 +10,6 @@ if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_P
return;
}
$getTags = DB::prepare('
SELECT
t.`tag_id`, t.`tag_name`, t.`tag_description`, t.`tag_created`,
(
SELECT COUNT(ct.`change_id`)
FROM `msz_changelog_change_tags` as ct
WHERE ct.`tag_id` = t.`tag_id`
) as `tag_count`
FROM `msz_changelog_tags` as t
ORDER BY t.`tag_id` ASC
');
Template::render('manage.changelog.tags', [
'changelog_tags' => $getTags->fetchAll(),
'changelog_tags' => ChangelogTag::all(),
]);

View file

@ -13,8 +13,8 @@ if(!user_session_active()) {
}
$errors = [];
$currentUserId = user_session_current('user_id');
$currentUser = User::byId($currentUserId);
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
$currentEmail = user_email_get($currentUserId);
$isRestricted = user_warning_check_restriction($currentUserId);
$twoFactorInfo = user_totp_info($currentUserId);

View file

@ -26,8 +26,8 @@ function db_to_zip(ZipArchive $archive, int $userId, string $filename, string $q
}
$errors = [];
$currentUserId = user_session_current('user_id');
$currentUser = User::byId($currentUserId);
$currentUser = User::getCurrent();
$currentUserId = $currentUser->getId();
if(isset($_POST['action']) && is_string($_POST['action'])) {
if(isset($_POST['password']) && is_string($_POST['password'])

View file

@ -0,0 +1,281 @@
<?php
namespace Misuzu\Changelog;
use JsonSerializable;
use UnexpectedValueException;
use Misuzu\DB;
use Misuzu\Memoizer;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class ChangelogChangeException extends ChangelogException {}
class ChangelogChangeNotFoundException extends ChangelogChangeException {}
class ChangelogChange implements JsonSerializable {
public const ACTION_UNKNOWN = 1;
public const ACTION_ADD = 1;
public const ACTION_REMOVE = 2;
public const ACTION_UPDATE = 3;
public const ACTION_FIX = 4;
public const ACTION_IMPORT = 5;
public const ACTION_REVERT = 6;
private const ACTION_STRINGS = [
self::ACTION_UNKNOWN => ['unknown', 'Changed'],
self::ACTION_ADD => ['add', 'Added'],
self::ACTION_REMOVE => ['remove', 'Removed'],
self::ACTION_UPDATE => ['update', 'Updated'],
self::ACTION_FIX => ['fix', 'Fixed'],
self::ACTION_IMPORT => ['import', 'Imported'],
self::ACTION_REVERT => ['revert', 'Reverted'],
];
public const DEFAULT_DATE = '0000-00-00';
// Database fields
private $change_id = -1;
private $user_id = null;
private $change_action = null; // defaults null apparently, probably a previous oversight
private $change_created = null;
private $change_log = '';
private $change_text = '';
private $user = null;
private $userLookedUp = false;
private $comments = null;
private $tags = null;
private $tagRelations = null;
public const TABLE = 'changelog_changes';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`change_id`, %1$s.`user_id`, %1$s.`change_action`, %1$s.`change_log`, %1$s.`change_text`'
. ', UNIX_TIMESTAMP(%1$s.`change_created`) AS `change_created`';
public function getId(): int {
return $this->change_id < 1 ? -1 : $this->change_id;
}
public function getUserId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
public function setUserId(?int $userId): self {
$this->user_id = $userId;
$this->userLookedUp = false;
$this->user = null;
return $this;
}
public function getUser(): ?User {
if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
$this->userLookedUp = true;
try {
$this->user = User::byId($userId);
} catch(UserNotFoundException $ex) {}
}
return $this->user;
}
public function setUser(?User $user): self {
$this->user_id = $user === null ? null : $user->getId();
$this->userLookedUp = true;
$this->user = $user;
return $this;
}
public function getAction(): int {
return $this->change_action ?? self::ACTION_UNKNOWN;
}
public function setAction(int $actionId): self {
$this->change_action = $actionId;
return $this;
}
private function getActionInfo(): array {
return self::ACTION_STRINGS[$this->getAction()] ?? self::ACTION_STRINGS[self::ACTION_UNKNOWN];
}
public function getActionClass(): string {
return $this->getActionInfo()[0];
}
public function getActionString(): string {
return $this->getActionInfo()[1];
}
public function getCreatedTime(): int {
return $this->change_created ?? -1;
}
public function getDate(): string {
return ($time = $this->getCreatedTime()) < 0 ? self::DEFAULT_DATE : gmdate('Y-m-d', $time);
}
public function getHeader(): string {
return $this->change_log;
}
public function setHeader(string $header): self {
$this->change_log = $header;
return $this;
}
public function getBody(): string {
return $this->change_text ?? '';
}
public function setBody(string $body): self {
$this->change_text = $body;
return $this;
}
public function hasBody(): bool {
return !empty($this->change_text);
}
public function getParsedBody(): string {
return Parser::instance(Parser::MARKDOWN)->parseText($this->getBody());
}
public function getCommentsCategoryName(): ?string {
return ($date = $this->getDate()) === self::DEFAULT_DATE ? null : sprintf('changelog-date-%s', $this->getDate());
}
public function hasCommentsCategory(): bool {
return $this->getCreatedTime() >= 0;
}
public function getCommentsCategory(): CommentsCategory {
if($this->comments === null) {
$categoryName = $this->getCommentsCategoryName();
if(empty($categoryName))
throw new UnexpectedValueException('Change comments category name is empty.');
try {
$this->comments = CommentsCategory::byName($categoryName);
} catch(CommentsCategoryNotFoundException $ex) {
$this->comments = new CommentsCategory($categoryName);
$this->comments->save();
}
}
return $this->comments;
}
public function getTags(): array {
if($this->tags === null)
$this->tags = ChangelogTag::byChange($this);
return $this->tags;
}
public function getTagRelations(): array {
if($this->tagRelations === null)
$this->tagRelations = ChangelogChangeTag::byChange($this);
return $this->tagRelations;
}
public function setTags(array $tags): self {
ChangelogChangeTag::purgeChange($this);
foreach($tags as $tag)
if($tag instanceof ChangelogTag)
ChangelogChangeTag::create($this, $tag);
$this->tags = $tags;
$this->tagRelations = null;
return $this;
}
public function hasTag(ChangelogTag $other): bool {
foreach($this->getTags() as $tag)
if($tag->compare($other))
return true;
return false;
}
public function jsonSerialize() {
return [
'id' => $this->getId(),
'user' => $this->getUserId(),
'action' => $this->getActionId(),
'header' => $this->getHeader(),
'body' => $this->getBody(),
'comments' => $this->getCommentsCategoryName(),
'created' => ($time = $this->getCreatedTime()) < 0 ? null : date('c', $time),
];
}
public function save(): void {
$isInsert = $this->getId() < 1;
if($isInsert) {
$query = 'INSERT INTO `%1$s%2$s` (`user_id`, `change_action`, `change_log`, `change_text`)'
. ' VALUES (:user, :action, :header, :body)';
} else {
$query = 'UPDATE `%1$s%2$s` SET `user_id` = :user, `change_action` = :action, `change_log` = :header, `change_text` = :body'
. ' WHERE `change_id` = :change';
}
$saveChange = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
->bind('user', $this->user_id)
->bind('action', $this->change_action)
->bind('header', $this->change_log)
->bind('body', $this->change_text);
if($isInsert) {
$this->change_id = $saveChange->executeGetId();
$this->change_created = time();
} else {
$saveChange->bind('change', $this->getId())
->execute();
}
}
private static function countQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`change_id`)', self::TABLE));
}
public static function countAll(?int $date = null, ?User $user = null): int {
$countChanges = DB::prepare(
self::countQueryBase()
. ' WHERE 1' // this is still disgusting
. ($date === null ? '' : ' AND DATE(`change_created`) = :date')
. ($user === null ? '' : ' AND `user_id` = :user')
);
if($date !== null)
$countChanges->bind('date', gmdate('Y-m-d', $date));
if($user !== null)
$countChanges->bind('user', $user->getId());
return (int)$countChanges->fetchColumn();
}
private static function memoizer(): Memoizer {
static $memoizer = null;
if($memoizer === null)
$memoizer = new Memoizer;
return $memoizer;
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $changeId): self {
return self::memoizer()->find($changeId, function() use ($changeId) {
$change = DB::prepare(self::byQueryBase() . ' WHERE `change_id` = :change')
->bind('change', $changeId)
->fetchObject(self::class);
if(!$change)
throw new ChangelogChangeNotFoundException;
return $change;
});
}
public static function all(?Pagination $pagination = null, ?int $date = null, ?User $user = null): array {
$changeQuery = self::byQueryBase()
. ' WHERE 1' // this is still disgusting
. ($date === null ? '' : ' AND DATE(`change_created`) = :date')
. ($user === null ? '' : ' AND `user_id` = :user')
. ' GROUP BY `change_created`, `change_id`'
. ' ORDER BY `change_created` DESC, `change_id` DESC';
if($pagination !== null)
$changeQuery .= ' LIMIT :range OFFSET :offset';
$getChanges = DB::prepare($changeQuery);
if($date !== null)
$getChanges->bind('date', gmdate('Y-m-d', $date));
if($user !== null)
$getChanges->bind('user', $user->getId());
if($pagination !== null)
$getChanges->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getChanges->fetchObjects(self::class);
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Misuzu\Changelog;
use JsonSerializable;
use Misuzu\DB;
class ChangelogChangeTagException extends ChangelogException {}
class ChangelogChangeTagNotFoundException extends ChangelogChangeTagException {}
class ChangelogChangeCreationFailedException extends ChangelogChangeTagException {}
class ChangelogChangeTag implements JsonSerializable {
// Database fields
private $change_id = -1;
private $tag_id = -1;
private $change = null;
private $tag = null;
public const TABLE = 'changelog_change_tags';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`change_id`, %1$s.`tag_id`';
public function getChangeId(): int {
return $this->change_id < 1 ? -1 : $this->change_id;
}
public function getChange(): ChangelogChange {
if($this->change === null)
$this->change = ChangelogChange::byId($this->getChangeId());
return $this->change;
}
public function getTagId(): int {
return $this->tag_id < 1 ? -1 : $this->tag_id;
}
public function getTag(): ChangelogTag {
if($this->tag === null)
$this->tag = ChangelogTag::byId($this->getTagId());
return $this->tag;
}
public function jsonSerialize() {
return [
'change' => $this->getChangeId(),
'tag' => $this->getTagId(),
];
}
public static function create(ChangelogChange $change, ChangelogTag $tag, bool $return = false): ?self {
$createRelation = DB::prepare(
'REPLACE INTO `' . DB::PREFIX . self::TABLE . '` (`change_id`, `tag_id`)'
. ' VALUES (:change, :tag)'
)->bind('change', $change->getId())->bind('tag', $tag->getId());
if(!$createRelation->execute())
throw new ChangelogChangeCreationFailedException;
if(!$return)
return null;
return self::byExact($change, $tag);
}
public static function purgeChange(ChangelogChange $change): void {
DB::prepare(
'DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `change_id` = :change'
)->bind('change', $change->getId())->execute();
}
private static function countQueryBase(string $column): string {
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`%s`)', self::TABLE, $column));
}
public static function countByTag(ChangelogTag $tag): int {
return (int)DB::prepare(
self::countQueryBase('change_id')
. ' WHERE `tag_id` = :tag'
)->bind('tag', $tag->getId())->fetchColumn();
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byExact(ChangelogChange $change, ChangelogTag $tag): array {
$tag = DB::prepare(self::byQueryBase() . ' WHERE `tag_id` = :tag')
->bind('change', $change->getId())
->bind('tag', $tag->getId())
->fetchObject(self::class);
if(!$tag)
throw new ChangelogChangeTagNotFoundException;
return $tag;
}
public static function byChange(ChangelogChange $change): array {
return DB::prepare(
self::byQueryBase()
. ' WHERE `change_id` = :change'
)->bind('change', $change->getId())->fetchObjects(self::class);
}
}

View file

@ -0,0 +1,6 @@
<?php
namespace Misuzu\Changelog;
use Exception;
class ChangelogException extends Exception {}

View file

@ -0,0 +1,140 @@
<?php
namespace Misuzu\Changelog;
use JsonSerializable;
use Misuzu\DB;
use Misuzu\Memoizer;
class ChangelogTagException extends ChangelogException {}
class ChangelogTagNotFoundException extends ChangelogTagException {}
class ChangelogTag implements JsonSerializable {
// Database fields
private $tag_id = -1;
private $tag_name = '';
private $tag_description = '';
private $tag_created = null;
private $tag_archived = null;
private $changeCount = -1;
public const TABLE = 'changelog_tags';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`tag_id`, %1$s.`tag_name`, %1$s.`tag_description`'
. ', UNIX_TIMESTAMP(%1$s.`tag_created`) AS `tag_created`'
. ', UNIX_TIMESTAMP(%1$s.`tag_archived`) AS `tag_archived`';
public function getId(): int {
return $this->tag_id < 1 ? -1 : $this->tag_id;
}
public function getName(): string {
return $this->tag_name;
}
public function setName(string $name): self {
$this->tag_name = $name;
return $this;
}
public function getDescription(): string {
return $this->tag_description;
}
public function hasDescription(): bool {
return !empty($this->tag_description);
}
public function setDescription(string $description): self {
$this->tag_description = $description;
return $this;
}
public function getCreatedTime(): int {
return $this->tag_created ?? -1;
}
public function getArchivedTime(): int {
return $this->tag_archived ?? -1;
}
public function isArchived(): bool {
return $this->getArchivedTime() >= 0;
}
public function setArchived(bool $archived): self {
if($this->isArchived() !== $archived)
$this->tag_archived = $archived ? time() : null;
return $this;
}
public function getChangeCount(): int {
if($this->changeCount < 0)
$this->changeCount = ChangelogChangeTag::countByTag($this);
return $this->changeCount;
}
public function jsonSerialize() {
return [
'id' => $this->getId(),
'name' => $this->getName(),
'description' => $this->getDescription(),
'created' => ($time = $this->getCreatedTime()) < 0 ? null : date('c', $time),
'archived' => ($time = $this->getArchivedTime()) < 0 ? null : date('c', $time),
];
}
public function compare(ChangelogTag $other): bool {
return $other === $this || $other->getId() === $this->getId();
}
public function save(): void {
$isInsert = $this->getId() < 1;
if($isInsert) {
$query = 'INSERT INTO `%1$s%2$s` (`tag_name`, `tag_description`, `tag_archived`)'
. ' VALUES (:name, :description, FROM_UNIXTIME(:archived))';
} else {
$query = 'UPDATE `%1$s%2$s` SET `tag_name` = :name, `tag_description` = :description, `tag_archived` = FROM_UNIXTIME(:archived)'
. ' WHERE `tag_id` = :tag';
}
$saveTag = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
->bind('name', $this->tag_name)
->bind('description', $this->tag_description)
->bind('archived', $this->tag_archived);
if($isInsert) {
$this->tag_id = $saveTag->executeGetId();
$this->tag_created = time();
} else {
$saveTag->bind('tag', $this->getId())
->execute();
}
}
private static function memoizer(): Memoizer {
static $memoizer = null;
if($memoizer === null)
$memoizer = new Memoizer;
return $memoizer;
}
private static function byQueryBase(): string {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $tagId): self {
return self::memoizer()->find($tagId, function() use ($tagId) {
$tag = DB::prepare(self::byQueryBase() . ' WHERE `tag_id` = :tag')
->bind('tag', $tagId)
->fetchObject(self::class);
if(!$tag)
throw new ChangelogTagNotFoundException;
return $tag;
});
}
public static function byChange(ChangelogChange $change): array {
return DB::prepare(
self::byQueryBase()
. ' WHERE `tag_id` IN (SELECT `tag_id` FROM `' . DB::PREFIX . ChangelogChangeTag::TABLE . '` WHERE `change_id` = :change)'
)->bind('change', $change->getId())->fetchObjects(self::class);
}
public static function all(): array {
return DB::prepare(self::byQueryBase())
->fetchObjects(self::class);
}
}

View file

@ -110,7 +110,7 @@ class CommentsCategory implements JsonSerializable {
return CommentsVote::byCategory($this, $user, $rootOnly, $pagination);
}
private static function getMemoizer() {
private static function memoizer() {
static $memoizer = null;
if($memoizer === null)
$memoizer = new Memoizer;
@ -121,7 +121,7 @@ class CommentsCategory implements JsonSerializable {
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
}
public static function byId(int $categoryId): self {
return self::getMemoizer()->find($categoryId, function() use ($categoryId) {
return self::memoizer()->find($categoryId, function() use ($categoryId) {
$cat = DB::prepare(self::byQueryBase() . ' WHERE `category_id` = :cat_id')
->bind('cat_id', $categoryId)
->fetchObject(self::class);
@ -131,7 +131,7 @@ class CommentsCategory implements JsonSerializable {
});
}
public static function byName(string $categoryName): self {
return self::getMemoizer()->find(function($category) use ($categoryName) {
return self::memoizer()->find(function($category) use ($categoryName) {
return $category->getName() === $categoryName;
}, function() use ($categoryName) {
$cat = DB::prepare(self::byQueryBase() . ' WHERE `category_name` = :name')

View file

@ -73,6 +73,7 @@ class CommentsPost implements JsonSerializable {
}
public function setUserId(int $userId): self {
$this->user_id = $userId < 1 ? null : $userId;
$this->userLookedUp = false;
$this->user = null;
return $this;
}
@ -87,6 +88,7 @@ class CommentsPost implements JsonSerializable {
}
public function setUser(?User $user): self {
$this->user_id = $user === null ? null : $user->getId();
$this->userLookedUp = true;
$this->user = $user;
return $this;
}

View file

@ -106,7 +106,7 @@ class CommentsVote implements JsonSerializable {
if(!$return)
return null;
return CommentsVote::byExact($post, $user);
return self::byExact($post, $user);
}
public static function delete(CommentsPost $post, User $user): void {

View file

@ -0,0 +1,131 @@
<?php
namespace Misuzu\Http\Handlers;
use ErrorException;
use HttpResponse;
use HttpRequest;
use Misuzu\Config;
use Misuzu\Pagination;
use Misuzu\Changelog\ChangelogChange;
use Misuzu\Changelog\ChangelogChangeNotFoundException;
use Misuzu\Changelog\ChangelogTag;
use Misuzu\Changelog\ChangelogTagNotFoundException;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class ChangelogHandler extends Handler {
public function index(HttpResponse $response, HttpRequest $request) {
$filterDate = $request->getQueryParam('date', FILTER_SANITIZE_STRING);
$filterUser = $request->getQueryParam('user', FILTER_SANITIZE_NUMBER_INT);
//$filterTags = $request->getQueryParam('tags');
if($filterDate !== null)
try {
$dateParts = explode('-', $filterDate, 3);
$filterDate = gmmktime(12, 0, 0, $dateParts[1], $dateParts[2], $dateParts[0]);
} catch(ErrorException $ex) {
return 404;
}
if($filterUser !== null)
try {
$filterUser = User::byId($filterUser);
} catch(UserNotFoundException $ex) {
return 404;
}
/*if($filterTags !== null) {
$splitTags = explode(',', $filterTags);
$filterTags = [];
for($i = 0; $i < min(10, count($splitTags)); ++$i)
try {
$filterTags[] = ChangelogTag::byId($splitTags[$i]);
} catch(ChangelogTagNotFoundException $ex) {
return 404;
}
}*/
$count = $filterDate !== null ? -1 : ChangelogChange::countAll($filterDate, $filterUser);
$pagination = new Pagination($count, 30);
if(!$pagination->hasValidOffset())
return 404;
$changes = ChangelogChange::all($pagination, $filterDate, $filterUser);
if(empty($changes))
return 404;
$response->setTemplate('changelog.index', [
'changelog_infos' => $changes,
'changelog_date' => $filterDate,
'changelog_user' => $filterUser,
'changelog_pagination' => $pagination,
'comments_user' => User::getCurrent(),
]);
}
public function change(HttpResponse $response, HttpRequest $request, int $changeId) {
try {
$changeInfo = ChangelogChange::byId($changeId);
} catch(ChangelogChangeNotFoundException $ex) {
return 404;
}
$response->setTemplate('changelog.change', [
'change_info' => $changeInfo,
'comments_user' => User::getCurrent(),
]);
}
private function createFeed(string $feedMode): Feed {
$changes = ChangelogChange::all(new Pagination(10));
$feed = (new Feed)
->setTitle(Config::get('site.name', Config::TYPE_STR, 'Misuzu') . ' » Changelog')
->setDescription('Live feed of changes to ' . Config::get('site.name', Config::TYPE_STR, 'Misuzu') . '.')
->setContentUrl(url_prefix(false) . url('changelog-index'))
->setFeedUrl(url_prefix(false) . url("changelog-feed-{$feedMode}"));
foreach($changes as $change) {
$changeUrl = url_prefix(false) . url('changelog-change', ['change' => $change->getId()]);
$commentsUrl = url_prefix(false) . url('changelog-change-comments', ['change' => $change->getId()]);
$feedItem = (new FeedItem)
->setTitle($change->getActionString() . ': ' . $change->getHeader())
->setCreationDate($change->getCreatedTime())
->setUniqueId($changeUrl)
->setContentUrl($changeUrl)
->setCommentsUrl($commentsUrl);
$feed->addItem($feedItem);
}
return $feed;
}
public function feedAtom(HttpResponse $response, HttpRequest $request) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed(self::createFeed('atom'));
}
public function feedRss(HttpResponse $response, HttpRequest $request) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed(self::createFeed('rss'));
}
public function legacy(HttpResponse $response, HttpRequest $request) {
$changeId = $request->getQueryParam('c', FILTER_SANITIZE_NUMBER_INT);
if($changeId) {
$response->redirect(url('changelog-change', ['change' => $changeId]), true);
return;
}
$response->redirect(url('changelog-index', [
'date' => $request->getQueryParam('d', FILTER_SANITIZE_STRING),
'user' => $request->getQueryParam('u', FILTER_SANITIZE_NUMBER_INT),
]), true);
}
}

View file

@ -6,6 +6,7 @@ use HttpRequest;
use Misuzu\Config;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Changelog\ChangelogChange;
use Misuzu\News\NewsPost;
final class HomeHandler extends Handler {
@ -55,15 +56,7 @@ final class HomeHandler extends Handler {
) AS `count_forum_posts`
')->fetch();
$changelog = DB::query('
SELECT
`change_id`, `change_log`, `change_action`,
DATE(`change_created`) AS `change_date`,
!ISNULL(`change_text`) AS `change_has_text`
FROM `msz_changelog_changes`
ORDER BY `change_created` DESC
LIMIT 10
')->fetchAll();
$changelog = ChangelogChange::all(new Pagination(10));
$birthdays = user_session_active() ? user_get_birthdays() : [];

View file

@ -16,7 +16,6 @@ use Misuzu\News\NewsCategoryNotFoundException;
use Misuzu\News\NewsPostNotException;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
final class NewsHandler extends Handler {
public function index(HttpResponse $response, HttpRequest $request) {
@ -61,18 +60,13 @@ final class NewsHandler extends Handler {
if(!$postInfo->isPublished() || $postInfo->isDeleted())
return 404;
$postInfo->ensureCommentsSection();
$commentsInfo = $postInfo->getCommentSection();
try {
$commentsUser = User::byId(user_session_current('user_id', 0));
} catch(UserNotFoundException $ex) {
$commentsUser = null;
}
$postInfo->ensureCommentsCategory();
$commentsInfo = $postInfo->getCommentsCategory();
$response->setTemplate('news.post', [
'post_info' => $postInfo,
'comments_info' => $commentsInfo,
'comments_user' => $commentsUser,
'comments_user' => User::getCurrent(),
]);
}
@ -95,9 +89,9 @@ final class NewsHandler extends Handler {
$feedItem = (new FeedItem)
->setTitle($post->getTitle())
->setSummary(first_paragraph($post->getText()))
->setSummary($post->getFirstParagraph())
->setContent(Parser::instance(Parser::MARKDOWN)->parseText($post->getText()))
->setCreationDate(strtotime($post->getCreatedTime()))
->setCreationDate($post->getCreatedTime())
->setUniqueId($postUrl)
->setContentUrl($postUrl)
->setCommentsUrl($commentsUrl)

View file

@ -2,13 +2,14 @@
namespace Misuzu\News;
use ArrayAccess;
use JsonSerializable;
use Misuzu\DB;
use Misuzu\Pagination;
class NewsCategoryException extends NewsException {};
class NewsCategoryNotFoundException extends NewsCategoryException {};
class NewsCategory implements ArrayAccess {
class NewsCategory implements ArrayAccess, JsonSerializable {
// Database fields
private $category_id = -1;
private $category_name = '';
@ -57,6 +58,16 @@ class NewsCategory implements ArrayAccess {
return $this->category_created === null ? -1 : $this->category_created;
}
public function jsonSerialize() {
return [
'id' => $this->getId(),
'name' => $this->getName(),
'description' => $this->getDescription(),
'is_hidden' => $this->isHidden(),
'created' => ($time = $this->getCreatedTime()) < 0 ? null : date('c', $time),
];
}
// Purely cosmetic, use ::countAll for pagination
public function getPostCount(): int {
if($this->postCount < 0)

View file

@ -1,17 +1,19 @@
<?php
namespace Misuzu\News;
use JsonSerializable;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class NewsPostException extends NewsException {};
class NewsPostNotFoundException extends NewsPostException {};
class NewsPost {
class NewsPost implements JsonSerializable {
// Database fields
private $post_id = -1;
private $category_id = -1;
@ -69,6 +71,7 @@ class NewsPost {
}
public function setUserId(int $userId): self {
$this->user_id = $userId < 1 ? null : $userId;
$this->userLookedUp = false;
$this->user = null;
return $this;
}
@ -83,19 +86,20 @@ class NewsPost {
}
public function setUser(?User $user): self {
$this->user_id = $user === null ? null : $user->getId();
$this->userLookedUp = true;
$this->user = $user;
return $this;
}
public function getCommentSectionId(): int {
public function getCommentsCategoryId(): int {
return $this->comment_section_id < 1 ? -1 : $this->comment_section_id;
}
public function hasCommentSection(): bool {
return $this->getCommentSectionId() > 0;
public function hasCommentsCategory(): bool {
return $this->getCommentsCategoryId() > 0;
}
public function getCommentSection(): CommentsCategory {
public function getCommentsCategory(): CommentsCategory {
if($this->comments === null)
$this->comments = CommentsCategory::byId($this->getCommentSectionId());
$this->comments = CommentsCategory::byId($this->getCommentsCategoryId());
return $this->comments;
}
@ -122,6 +126,15 @@ class NewsPost {
$this->post_text = $text;
return $this;
}
public function getParsedText(): string {
return Parser::instance(Parser::MARKDOWN)->parseText($this->getText());
}
public function getFirstParagraph(): string {
return first_paragraph($this->getText());
}
public function getParsedFirstParagraph(): string {
return Parser::instance(Parser::MARKDOWN)->parseText($this->getFirstParagraph());
}
public function getScheduledTime(): int {
return $this->post_scheduled === null ? -1 : $this->post_scheduled;
@ -158,18 +171,33 @@ class NewsPost {
return $this;
}
public function ensureCommentsSection(): void {
if($this->hasCommentSection())
public function jsonSerialize() {
return [
'id' => $this->getId(),
'category' => $this->getCategoryId(),
'user' => $this->getUserId(),
'comments' => $this->getCommentsCategoryId(),
'is_featured' => $this->isFeatured(),
'title' => $this->getTitle(),
'text' => $this->getText(),
'scheduled' => ($time = $this->getScheduledTime()) < 0 ? null : date('c', $time),
'created' => ($time = $this->getCreatedTime()) < 0 ? null : date('c', $time),
'updated' => ($time = $this->getUpdatedTime()) < 0 ? null : date('c', $time),
'deleted' => ($time = $this->getDeletedTime()) < 0 ? null : date('c', $time),
];
}
public function ensureCommentsCategory(): void {
if($this->hasCommentsCategory())
return;
$this->comments = (new CommentsCategory)
->setName("news-{$this->getId()}");
$this->comments = new CommentsCategory("news-{$this->getId()}");
$this->comments->save();
$this->comment_section_id = $this->comments->getId();
DB::prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :comment_section_id WHERE `post_id` = :post_id')
->execute([
'comment_section_id' => $this->getCommentSectionId(),
'comment_section_id' => $this->getCommentsCategoryId(),
'post_id' => $this->getId(),
]);
}

View file

@ -11,7 +11,6 @@ final class TwigMisuzu extends Twig_Extension {
return [
new Twig_Filter('html_colour', 'html_colour'),
new Twig_Filter('country_name', 'get_country_name'),
new Twig_Filter('first_paragraph', 'first_paragraph'),
new Twig_Filter('byte_symbol', 'byte_symbol'),
new Twig_Filter('html_link', 'html_link'),
// deprecate this call, convert to html in php
@ -32,7 +31,6 @@ final class TwigMisuzu extends Twig_Extension {
new Twig_Function('url_list', 'url_list'),
new Twig_Function('html_tag', 'html_tag'),
new Twig_Function('html_avatar', 'html_avatar'),
new Twig_Function('changelog_action_name', 'changelog_action_name'),
new Twig_Function('forum_may_have_children', 'forum_may_have_children'),
new Twig_Function('forum_may_have_topics', 'forum_may_have_topics'),
new Twig_Function('forum_has_priority_voting', 'forum_has_priority_voting'),

View file

@ -34,6 +34,8 @@ class User {
public $user_background_settings = 0;
public $user_title = null;
private static $localUser = null;
private const USER_SELECT = '
SELECT u.`user_id`, u.`username`, u.`password`, u.`email`, u.`user_super`, u.`user_title`,
u.`user_country`, u.`user_colour`, u.`display_role`, u.`user_totp_key`,
@ -55,32 +57,6 @@ class User {
//
}
public static function create(
string $username,
string $password,
string $email,
string $ipAddress
): ?User {
$createUser = DB::prepare('
INSERT INTO `msz_users` (
`username`, `password`, `email`, `register_ip`,
`last_ip`, `user_country`, `display_role`
) VALUES (
:username, :password, LOWER(:email), INET6_ATON(:register_ip),
INET6_ATON(:last_ip), :user_country, 1
)
') ->bind('username', $username)->bind('email', $email)
->bind('register_ip', $ipAddress)->bind('last_ip', $ipAddress)
->bind('password', user_password_hash($password))
->bind('user_country', IPAddress::country($ipAddress))
->executeGetId();
if($createUser < 1)
return null;
return static::byId($createUser);
}
public function getId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
@ -127,6 +103,10 @@ class User {
return !empty($this->user_deleted);
}
public function getDisplayRoleId(): int {
return $this->display_role < 1 ? -1 : $this->display_role;
}
public function hasTOTP(): bool {
return !empty($this->user_totp_key);
}
@ -144,6 +124,10 @@ class User {
return ($this->user_background_settings & MSZ_USER_BACKGROUND_ATTRIBUTE_SLIDE) > 0;
}
public function getTitle(): string {
return $this->user_title;
}
public function profileFields(bool $filterEmpty = true): array {
if(($userId = $this->getId()) < 1)
return [];
@ -165,6 +149,42 @@ class User {
return $this->commentPermsArray;
}
public function setCurrent(): void {
self::$localUser = $this;
}
public static function unsetCurrent(): void {
self::$localUser = null;
}
public static function getCurrent(): ?User {
return self::$localUser;
}
public static function create(
string $username,
string $password,
string $email,
string $ipAddress
): ?User {
$createUser = DB::prepare('
INSERT INTO `msz_users` (
`username`, `password`, `email`, `register_ip`,
`last_ip`, `user_country`, `display_role`
) VALUES (
:username, :password, LOWER(:email), INET6_ATON(:register_ip),
INET6_ATON(:last_ip), :user_country, 1
)
') ->bind('username', $username)->bind('email', $email)
->bind('register_ip', $ipAddress)->bind('last_ip', $ipAddress)
->bind('password', user_password_hash($password))
->bind('user_country', IPAddress::country($ipAddress))
->executeGetId();
if($createUser < 1)
return null;
return static::byId($createUser);
}
private static function getMemoizer() {
static $memoizer = null;
if($memoizer === null)
@ -184,7 +204,7 @@ class User {
}
public static function findForLogin(string $usernameOrEmail): ?User {
$usernameOrEmailLower = mb_strtolower($usernameOrEmail);
return self::getMemoizer()->find(function() use ($usernameOrEmailLower) {
return self::getMemoizer()->find(function($user) use ($usernameOrEmailLower) {
return mb_strtolower($user->getUsername()) === $usernameOrEmailLower
|| mb_strtolower($user->getEmailAddress()) === $usernameOrEmailLower;
}, function() use ($usernameOrEmail) {
@ -199,7 +219,7 @@ class User {
}
public static function findForProfile($userIdOrName): ?User {
$userIdOrNameLower = mb_strtolower($userIdOrName);
return self::getMemoizer()->find(function() use ($userIdOrNameLower) {
return self::getMemoizer()->find(function($user) use ($userIdOrNameLower) {
return $user->getId() == $userIdOrNameLower || mb_strtolower($user->getUsername()) === $userIdOrNameLower;
}, function() use ($userIdOrName) {
$user = DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id OR LOWER(`username`) = LOWER(:username)')

View file

@ -1,131 +0,0 @@
<?php
define('MSZ_CHANGELOG_ACTION_ADD', 1);
define('MSZ_CHANGELOG_ACTION_REMOVE', 2);
define('MSZ_CHANGELOG_ACTION_UPDATE', 3);
define('MSZ_CHANGELOG_ACTION_FIX', 4);
define('MSZ_CHANGELOG_ACTION_IMPORT', 5);
define('MSZ_CHANGELOG_ACTION_REVERT', 6);
define('MSZ_CHANGELOG_ACTIONS', [
MSZ_CHANGELOG_ACTION_ADD => 'add',
MSZ_CHANGELOG_ACTION_REMOVE => 'remove',
MSZ_CHANGELOG_ACTION_UPDATE => 'update',
MSZ_CHANGELOG_ACTION_FIX => 'fix',
MSZ_CHANGELOG_ACTION_IMPORT => 'import',
MSZ_CHANGELOG_ACTION_REVERT => 'revert',
]);
function changelog_action_name(int $action): string {
return changelog_action_is_valid($action) ? MSZ_CHANGELOG_ACTIONS[$action] : '';
}
function changelog_action_is_valid(int $action): bool {
return array_key_exists($action, MSZ_CHANGELOG_ACTIONS);
}
define('MSZ_CHANGELOG_GET_QUERY', '
SELECT
c.`change_id`, c.`change_log`, c.`change_action`,
u.`user_id`, u.`username`,
DATE(`change_created`) AS `change_date`,
!ISNULL(c.`change_text`) AS `change_has_text`,
COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`
FROM `msz_changelog_changes` AS c
LEFT JOIN `msz_users` AS u
ON u.`user_id` = c.`user_id`
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = u.`display_role`
WHERE %s
AND %s
GROUP BY `change_created`, `change_id`
ORDER BY `change_created` DESC, `change_id` DESC
%s
');
function changelog_get_changes(string $date, int $user, int $offset, int $take): array {
$hasDate = mb_strlen($date) > 0;
$hasUser = $user > 0;
$query = sprintf(
MSZ_CHANGELOG_GET_QUERY,
$hasDate ? 'DATE(c.`change_created`) = :date' : '1',
$hasUser ? 'c.`user_id` = :user' : '1',
!$hasDate ? 'LIMIT :offset, :take' : ''
);
$prep = \Misuzu\DB::prepare($query);
if(!$hasDate) {
$prep->bind('offset', $offset);
$prep->bind('take', $take);
} else {
$prep->bind('date', $date);
}
if($hasUser) {
$prep->bind('user', $user);
}
return $prep->fetchAll();
}
define('MSZ_CHANGELOG_COUNT_QUERY', '
SELECT COUNT(`change_id`)
FROM `msz_changelog_changes`
WHERE %s
AND %s
');
function changelog_count_changes(string $date, int $user): int {
$hasDate = mb_strlen($date) > 0;
$hasUser = $user > 0;
$query = sprintf(
MSZ_CHANGELOG_COUNT_QUERY,
$hasDate ? 'DATE(`change_created`) = :date' : '1',
$hasUser ? '`user_id` = :user' : '1'
);
$prep = \Misuzu\DB::prepare($query);
if($hasDate) {
$prep->bind('date', $date);
}
if($hasUser) {
$prep->bind('user', $user);
}
return (int)$prep->fetchColumn();
}
function changelog_change_get(int $changeId): array {
$getChange = \Misuzu\DB::prepare('
SELECT
c.`change_id`, c.`change_created`, c.`change_log`, c.`change_text`, c.`change_action`,
u.`user_id`, u.`username`, u.`display_role` AS `user_role`,
DATE(`change_created`) AS `change_date`,
COALESCE(u.`user_title`, r.`role_title`) AS `user_title`,
COALESCE(u.`user_colour`, r.`role_colour`) AS `user_colour`
FROM `msz_changelog_changes` AS c
LEFT JOIN `msz_users` AS u
ON u.`user_id` = c.`user_id`
LEFT JOIN `msz_roles` AS r
ON r.`role_id` = u.`display_role`
WHERE `change_id` = :change_id
');
$getChange->bind('change_id', $changeId);
return $getChange->fetch();
}
function changelog_change_tags_get(int $changeId): array {
$getTags = \Misuzu\DB::prepare('
SELECT
t.`tag_id`, t.`tag_name`, t.`tag_description`
FROM `msz_changelog_tags` as t
LEFT JOIN `msz_changelog_change_tags` as ct
ON ct.`tag_id` = t.`tag_id`
WHERE ct.`change_id` = :change_id
');
$getTags->bind('change_id', $changeId);
return $getTags->fetchAll();
}

View file

@ -24,10 +24,11 @@ define('MSZ_URLS', [
'auth-resolve-user' => ['/auth/login.php', ['resolve_user' => '<username>']],
'auth-two-factor' => ['/auth/twofactor.php', ['token' => '<token>']],
'changelog-index' => ['/changelog.php'],
'changelog-change' => ['/changelog.php', ['c' => '<change>']],
'changelog-date' => ['/changelog.php', ['d' => '<date>']],
'changelog-tag' => ['/changelog.php', ['t' => '<tag>']],
'changelog-index' => ['/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>']],
'changelog-feed-rss' => ['/changelog.rss'],
'changelog-feed-atom' => ['/changelog.atom'],
'changelog-change' => ['/changelog/change/<change>'],
'changelog-change-comments' => ['/changelog/change/<change>', [], 'comments'],
'news-index' => ['/news', ['p' => '<page>']],
'news-category' => ['/news/<category>', ['p' => '<page>']],

View file

@ -2,68 +2,52 @@
{% from 'macros.twig' import container_title, avatar %}
{% from '_layout/comments.twig' import comments_section %}
{% set title = 'Changelog » Change #' ~ change.change_id %}
{% set canonical_url = url('changelog-change', {'change': change.change_id}) %}
{% set manage_link = url('manage-changelog-change', {'change': change.change_id}) %}
{% set description = change.change_log %}
{% if change.change_action == constant('MSZ_CHANGELOG_ACTION_ADD') %}
{% set action_name = 'Added' %}
{% elseif change.change_action == constant('MSZ_CHANGELOG_ACTION_REMOVE') %}
{% set action_name = 'Removed' %}
{% elseif change.change_action == constant('MSZ_CHANGELOG_ACTION_UPDATE') %}
{% set action_name = 'Updated' %}
{% elseif change.change_action == constant('MSZ_CHANGELOG_ACTION_FIX') %}
{% set action_name = 'Fixed' %}
{% elseif change.change_action == constant('MSZ_CHANGELOG_ACTION_IMPORT') %}
{% set action_name = 'Imported' %}
{% elseif change.change_action == constant('MSZ_CHANGELOG_ACTION_REVERT') %}
{% set action_name = 'Reverted' %}
{% else %}
{% set action_name = 'Unknown' %}
{% endif %}
{% set title = 'Changelog » Change #' ~ change_info.id %}
{% set canonical_url = url('changelog-change', {'change': change_info.id}) %}
{% set manage_link = url('manage-changelog-change', {'change': change_info.id}) %}
{% set description = change_info.header %}
{% block content %}
<div class="container changelog__log changelog__action--{{ changelog_action_name(change.change_action) }}">
<div class="container changelog__log changelog__action--{{ change_info.actionClass }}">
<div class="changelog__log__action">
{{ action_name }}
{{ change_info.actionString }}
</div>
<div class="changelog__log__text">
{{ change.change_log }}
{{ change_info.header }}
</div>
</div>
<div class="container changelog__change" style="{% if change.user_colour is not null %}{{ change.user_colour|html_colour('--accent-colour') }}{% endif %}">
<div class="container changelog__change"{% if change_info.user is not null %} style="--accent-colour: {{ change_info.user.colour }}"{% endif %}>
<div class="changelog__change__info">
<div class="changelog__change__info__background"></div>
<div class="changelog__change__info__content">
{% if change.user_id is not null %}
{% if change_info.user.id is not null %}
<div class="changelog__change__user">
<a class="changelog__change__avatar" href="{{ url('user-profile', {'user': change.user_id}) }}">
{{ avatar(change.user_id, 60, change.username) }}
<a class="changelog__change__avatar" href="{{ url('user-profile', {'user': change_info.user.id}) }}">
{{ avatar(change_info.user.id, 60, change_info.user.username) }}
</a>
<div class="changelog__change__user__details">
<a class="changelog__change__username" href="{{ url('user-profile', {'user': change.user_id}) }}">{{ change.username }}</a>
<a class="changelog__change__userrole" href="{{ url('user-list', {'role': change.user_role}) }}">{{ change.user_title }}</a>
<a class="changelog__change__username" href="{{ url('user-profile', {'user': change_info.user.id}) }}">{{ change_info.user.username }}</a>
<a class="changelog__change__userrole" href="{{ url('user-list', {'role': change_info.user.displayRoleId}) }}">{{ change_info.user.title }}</a>
</div>
</div>
{% endif %}
<a class="changelog__change__date" href="{{ url('changelog-date', {'date': change.change_date}) }}">
<a class="changelog__change__date" href="{{ url('changelog-index', {'date': change_info.date}) }}">
Created
<time datetime="{{ change.change_created|date('c') }}" title="{{ change.change_created|date('r') }}">
{{ change.change_created|time_diff }}
<time datetime="{{ change_info.createdTime|date('c') }}" title="{{ change_info.createdTime|date('r') }}">
{{ change_info.createdTime|time_diff }}
</time>
</a>
{% if tags|length > 0 %}
{% if change_info.tags|length > 0 %}
<ul class="changelog__change__tags">
{% for tag in tags %}
<li class="changelog__change__tag" title="{{ tag.tag_description }}">
<a href="{{ url('changelog-tag', {'tag': tag.tag_id}) }}" class="changelog__change__tag__link">
{{ tag.tag_name }}
{% for tag in change_info.tags %}
<li class="changelog__change__tag" title="{{ tag.description }}">
<a href="{{ url('changelog-tag', {'tag': tag.id}) }}" class="changelog__change__tag__link">
{{ tag.name }}
</a>
</li>
{% endfor %}
@ -75,18 +59,18 @@
<div class="changelog__change__text markdown">
<h1>{{ title }}</h1>
{% if change.change_text|length >= 1 %}
{{ change.change_text|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
{% if change_info.hasBody %}
{{ change_info.parsedBody|raw }}
{% else %}
<p>This change has no additional notes.</p>
{% endif %}
</div>
</div>
{% if comments_category is defined %}
{% if change_info.hasCommentsCategory %}
<div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments for ' ~ change.change_date) }}
{{ comments_section(comments_category, comments_user) }}
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments for ' ~ change_info.date) }}
{{ comments_section(change_info.commentsCategory, comments_user) }}
</div>
{% endif %}
{% endblock %}

View file

@ -3,41 +3,51 @@
{% from 'changelog/macros.twig' import changelog_listing %}
{% from '_layout/comments.twig' import comments_section %}
{% set is_valid = changes|length > 0 %}
{% set is_date = changelog_date|length > 0 %}
{% set is_date = changelog_date > 0 %}
{% set is_user = changelog_user is not null %}
{% set title = 'Changelog' %}
{% set changelog_date_fmt = changelog_date|default(false) ? changelog_date|date('Y-m-d') : '' %}
{% if is_valid %}
{%
set canonical_url = url_construct(url('changelog-index'), {
'd': changelog_date,
'u': changelog_user|default(0),
'p': changelog_pagination.page,
})
%}
{% set canonical_url = url_construct(url('changelog-index'), {
'd': changelog_date_fmt,
'u': changelog_user.id|default(0),
'p': changelog_pagination.page,
}) %}
{% if is_date or changelog_user %}
{% set title = title ~ ' »' ~ (changelog_date ? ' ' ~ changes[0].change_date : '') ~ (changelog_user ? ' by ' ~ changes[0].username : '') %}
{% endif %}
{% if is_date or is_user %}
{% set title = title ~ ' »' ~ (is_date ? ' ' ~ changelog_infos[0].date : '') ~ (is_user ? ' by ' ~ changelog_infos[0].user.username : '') %}
{% else %}
{% set feeds = [
{
'type': 'rss',
'title': '',
'url': url('changelog-feed-rss'),
},
{
'type': 'atom',
'title': '',
'url': url('changelog-feed-atom'),
},
] %}
{% endif %}
{% block content %}
<div class="container changelog__container">
{{ container_title('<i class="fas fa-wrench fa-fw"></i> ' ~ title) }}
{{ changelog_listing(changes, is_date) }}
{{ changelog_listing(changelog_infos, is_date) }}
{% if not is_date %}
<div class="changelog__pagination">
{{ pagination(changelog_pagination, url('changelog-index'), null, {'d':changelog_date, 'u':changelog_user|default(0)})}}
{{ pagination(changelog_pagination, url('changelog-index'), null, {'date':changelog_date_fmt, 'user':changelog_user.id|default(0)})}}
</div>
{% endif %}
</div>
{% if comments_category is defined %}
{% if is_date %}
<div class="container">
{{ container_title('<i class="fas fa-comments fa-fw"></i> Comments') }}
{{ comments_section(comments_category, comments_user) }}
{{ comments_section(changelog_infos[0].commentsCategory, comments_user) }}
</div>
{% endif %}
{% endblock %}

View file

@ -4,10 +4,10 @@
<div class="changelog__listing">
{% if changes|length > 0 %}
{% for change in changes %}
{% if not hide_dates and (last_date is not defined or last_date != change.change_date) %}
{% set last_date = change.change_date %}
{% if not hide_dates and (last_date is not defined or last_date != change.date) %}
{% set last_date = change.date %}
<a href="{{ is_manage ? '#cd' ~ last_date : url('changelog-date', {'date': last_date}) }}" class="changelog__listing__date" id="cd{{ last_date }}">
<a href="{{ is_manage ? '#cd' ~ last_date : url('changelog-index', {'date': last_date}) }}" class="changelog__listing__date" id="cd{{ last_date }}">
{{ last_date }}
</a>
{% endif %}
@ -23,72 +23,52 @@
{% endmacro %}
{% macro changelog_entry(change, is_small, is_manage) %}
{% set change_url = url(is_manage ? 'manage-changelog-change' : 'changelog-change', {'change': change.change_id}) %}
{% set has_text = change.change_has_text|default(false)
or (change.change_text is defined and change.change_text|length > 0)
%}
{% set change_url = url(is_manage ? 'manage-changelog-change' : 'changelog-change', {'change': change.id}) %}
<div class="changelog__entry" id="cl{{ change.change_id }}">
<div class="changelog__entry" id="cl{{ change.id }}">
<div class="changelog__entry__info">
{% if is_manage %}
<a href="{{ change_url|format(change.change_id) }}" class="changelog__entry__datetime">
<a href="{{ change_url }}" class="changelog__entry__datetime">
<time class="changelog__datetime__text"
datetime="{{ change.change_created|date('c') }}"
title="{{ change.change_created|date('r') }}">
{{ change.change_created|time_diff }}
datetime="{{ change.createdTime|date('c') }}"
title="{{ change.createdTime|date('r') }}">
{{ change.createdTime|time_diff }}
</time>
</a>
{% endif %}
{% if change.change_action == constant('MSZ_CHANGELOG_ACTION_ADD') %}
{% set action_name = 'Added' %}
{% elseif change.change_action == constant('MSZ_CHANGELOG_ACTION_REMOVE') %}
{% set action_name = 'Removed' %}
{% elseif change.change_action == constant('MSZ_CHANGELOG_ACTION_UPDATE') %}
{% set action_name = 'Updated' %}
{% elseif change.change_action == constant('MSZ_CHANGELOG_ACTION_FIX') %}
{% set action_name = 'Fixed' %}
{% elseif change.change_action == constant('MSZ_CHANGELOG_ACTION_IMPORT') %}
{% set action_name = 'Imported' %}
{% elseif change.change_action == constant('MSZ_CHANGELOG_ACTION_REVERT') %}
{% set action_name = 'Reverted' %}
{% else %}
{% set action_name = 'Unknown' %}
{% endif %}
<a class="changelog__entry__action changelog__action--{{ changelog_action_name(change.change_action) }}"
href="{{ change_url|format(change.change_id) }}"
{% if change.action_colour is defined %}style="{{ change.action_colour|html_colour('--action-colour') }}"{% endif %}
{% if is_small %}title="{{ action_name }}"{% endif %}>
<a class="changelog__entry__action changelog__action--{{ change.actionClass }}"
href="{{ change_url }}"
{% if is_small %}title="{{ change.actionString }}"{% endif %}>
{% if not is_small %}
<div class="changelog__entry__action__text">
{{ action_name }}
{{ change.actionString }}
</div>
{% endif %}
</a>
{% if change.user_id is defined %}
{% if not is_small %}
<a class="changelog__entry__user"
href="{{ url(is_manage ? 'manage-user' : 'user-profile', {'user': change.user_id}) }}"
style="{{ change.user_colour|html_colour }}">
href="{{ url(is_manage ? 'manage-user' : 'user-profile', {'user': change.user.id}) }}"
style="--user-colour: {{ change.user.colour }}">
<div class="changelog__entry__user__text">
{{ change.username }}
{{ change.user.username }}
</div>
</a>
{% endif %}
</div>
<div class="changelog__entry__text">
<a class="changelog__entry__log{% if has_text %} changelog__entry__log--link{% endif %}"
{% if has_text %}href="{{ change_url|format(change.change_id) }}"{% endif %}>
{{ change.change_log }}
<a class="changelog__entry__log{% if change.hasBody %} changelog__entry__log--link{% endif %}"
{% if change.hasBody %}href="{{ change_url }}"{% endif %}>
{{ change.header }}
</a>
{% if is_manage %}
<div class="changelog__entry__tags">
{% for tag in change.tags %}
<a href="{{ url(is_manage ? 'manage-changelog-tag' : 'changelog-tag', {'tag': tag.tag_id}) }}" class="changelog__entry__tag">
{{ tag.tag_name }}
<a href="{{ url(is_manage ? 'manage-changelog-tag' : 'changelog-tag', {'tag': tag.id}) }}" class="changelog__entry__tag">
{{ tag.name }}
</a>
{% endfor %}
</div>

View file

@ -3,39 +3,39 @@
{% from '_layout/input.twig' import input_csrf, input_text, input_select, input_checkbox %}
{% if change is not null %}
{% set site_link = url('changelog-change', {'change': change.change_id}) %}
{% set site_link = url('changelog-change', {'change': change.id}) %}
{% endif %}
{% block manage_content %}
<div class="container">
<form action="{{ url('manage-changelog-change', {'change': change.change_id|default(0)}) }}" method="post">
<form action="{{ url('manage-changelog-change', {'change': change.id|default(0)}) }}" method="post">
{{ input_csrf() }}
{{ container_title(change is not null ? 'Editing #' ~ change.change_id : 'Adding a new change') }}
{{ container_title(change is not null ? 'Editing #' ~ change.id : 'Adding a new change') }}
<div style="display: flex; margin: 2px 5px;">
{{ input_select('change[action]', change_actions, change.change_action|default(0), 'action_name', 'action_id') }}
{{ input_text('change[log]', '', change is not null ? change.change_log : '', 'text', '', true, {'maxlength':255,'style':'flex-grow:1'}) }}
{{ input_select('change[action]', change_actions, change.action|default(0), 'action_name', 'action_id') }}
{{ input_text('change[log]', '', change.header|default(''), 'text', '', true, {'maxlength':255,'style':'flex-grow:1'}) }}
</div>
<label class="form__label">
<div class="form__label__text">Text</div>
<div class="form__label__input">
<textarea class="input__textarea" name="change[text]" maxlength="65535">{{ change is not null ? change.change_text : '' }}</textarea>
<textarea class="input__textarea" name="change[text]" maxlength="65535">{{ change.body|default('') }}</textarea>
</div>
</label>
<label class="form__label">
<div class="form__label__text">Contributor Id</div>
<div class="form__label__input">
{{ input_text('change[user]', '', change.user_id|default(current_user.user_id), 'number', '', false, {'min':1}) }}
{{ input_text('change[user]', '', change.userId|default(current_user.user_id), 'number', '', false, {'min':1}) }}
</div>
</label>
<label class="form__label">
<div class="form__label__text">Created</div>
<div class="form__label__input">
{{ input_text('change[created]', '', (change is not null ? change.change_created : ''|date('Y-m-d H:i:s')), 'text', '', true) }}
{{ input_text('change[created]', '', change.createdTime|default(null)|date('Y-m-d H:i:s'), 'text', '', true) }}
</div>
</label>
@ -44,9 +44,9 @@
<label class="manage__tag">
<div class="manage__tag__background"></div>
<div class="manage__tag__content">
{{ input_checkbox('tags[]', '', tag.has_tag, 'manage__tag__checkbox', tag.tag_id) }}
{{ input_checkbox('tags[]', '', change.hasTag(tag)|default(false), 'manage__tag__checkbox', tag.id) }}
<div class="manage__tag__title">
{{ tag.tag_name }}
{{ tag.name }}
</div>
</div>
</label>

View file

@ -4,29 +4,29 @@
{% block manage_content %}
<div class="container">
<form action="{{ url('manage-changelog-tag', {'tag': edit_tag.tag_id|default(0)}) }}" method="post">
<form action="{{ url('manage-changelog-tag', {'tag': edit_tag.id|default(0)}) }}" method="post">
{{ input_csrf() }}
{{ container_title(edit_tag is defined ? 'Editing ' ~ edit_tag.tag_name ~ ' (' ~ edit_tag.tag_id ~ ')' : 'Adding a new tag') }}
{{ container_title(edit_tag is defined ? 'Editing ' ~ edit_tag.name ~ ' (' ~ edit_tag.id ~ ')' : 'Adding a new tag') }}
<label class="form__label" style="width:100%">
<div class="form__label__text">Name</div>
<div class="form__label__input">
{{ input_text('tag[name]', '', edit_tag is defined ? edit_tag.tag_name : '', 'text', '', true, {'maxlength':255}) }}
{{ input_text('tag[name]', '', edit_tag is defined ? edit_tag.name : '', 'text', '', true, {'maxlength':255}) }}
</div>
</label>
<label class="form__label" style="width:100%">
<div class="form__label__text">Description</div>
<div class="form__label__input">
<textarea class="input__textarea" name="tag[description]" maxlength="65535">{{ edit_tag is defined ? edit_tag.tag_description : '' }}</textarea>
<textarea class="input__textarea" name="tag[description]" maxlength="65535">{{ edit_tag.description|default('') }}</textarea>
</div>
</label>
<label class="form__label">
<div class="form__label__text">Archived</div>
<div class="form__label__input">
{{ input_checkbox('tag[archived]', '', edit_tag is defined and edit_tag.tag_archived is not null) }}
{{ input_checkbox('tag[archived]', '', edit_tag.archived|default(false)) }}
</div>
</label>
@ -34,7 +34,7 @@
<label class="form__label">
<div class="form__label__text">Created</div>
<div class="form__label__input">
{{ input_text('', '', edit_tag.tag_created) }}
{{ input_text('', '', edit_tag.createdTime|date('r')) }}
</div>
</label>
{% endif %}

View file

@ -3,28 +3,26 @@
{% block manage_content %}
<div class="changelog-actions-tags">
{% if changelog_tags is defined %}
<div class="container changelog-actions-tags__panel changelog-actions-tags__panel--tags">
{{ container_title('Tags') }}
<div class="container changelog-actions-tags__panel changelog-actions-tags__panel--tags">
{{ container_title('Tags') }}
<a href="{{ url('manage-changelog-tag') }}" class="input__button">Create new tag</a>
<a href="{{ url('manage-changelog-tag') }}" class="input__button">Create new tag</a>
<div class="changelog-actions-tags__list">
{% for tag in changelog_tags %}
<a href="{{ url('manage-changelog-tag', {'tag': tag.tag_id}) }}" class="changelog-actions-tags__entry">
<div class="listing__entry__content changelog-tags__content">
<div class="changelog-tags__text">
{{ tag.tag_name }} ({{ tag.tag_count }})
</div>
<div class="changelog-tags__description">
{{ tag.tag_description }}
</div>
<div class="changelog-actions-tags__list">
{% for tag in changelog_tags %}
<a href="{{ url('manage-changelog-tag', {'tag': tag.id}) }}" class="changelog-actions-tags__entry">
<div class="listing__entry__content changelog-tags__content">
<div class="changelog-tags__text">
{{ tag.name }} ({{ tag.changeCount }})
</div>
</a>
{% endfor %}
</div>
<div class="changelog-tags__description">
{{ tag.description }}
</div>
</div>
</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -18,7 +18,7 @@
{{ post.createdTime|date('r') }},
{{ post.updatedTime|date('r') }},
{{ post.deletedTime|date('r') }},
{{ post.commentSectionId }}
{{ post.commentsCategoryId }}
</p>
{% endfor %}

View file

@ -33,12 +33,12 @@
<div class="news__preview__content markdown">
<h1>{{ post.title }}</h1>
<div class="news__preview__text">
{{ post.text|first_paragraph|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
{{ post.parsedFirstParagraph|raw }}
</div>
<div class="news__preview__links">
<a href="{{ url('news-post', {'post': post.id}) }}" class="news__preview__link">Continue reading</a>
<a href="{{ url('news-post-comments', {'post': post.id}) }}" class="news__preview__link">
{{ not post.hasCommentSection or post.commentSection.postCount < 1 ? 'No' : post.commentSection.postCount|number_format }} comment{{ not post.hasCommentSection or post.commentSection.postCount != 1 ? 's' : '' }}
{{ not post.hasCommentsCategory or post.commentsCategory.postCount < 1 ? 'No' : post.commentsCategory.postCount|number_format }} comment{{ not post.hasCommentsCategory or post.commentsCategory.postCount != 1 ? 's' : '' }}
</a>
</div>
</div>
@ -88,7 +88,7 @@
<div class="news__post__text markdown">
<h1>{{ post.title }}</h1>
{{ post.text|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
{{ post.parsedText|raw }}
</div>
</div>
{% endmacro %}