aboutsummaryrefslogtreecommitdiff
path: root/handler
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2022-07-09 19:40:17 +0300
committerEvgeny Zinoviev <me@ch1p.io>2022-07-09 19:40:17 +0300
commitf7bfdf58def6aadc922e1632f407d1418269a0d7 (patch)
treed7a0b2819e6a26c11d40ee0b27267ea827fbb345 /handler
initial
Diffstat (limited to 'handler')
-rw-r--r--handler/Auto.php106
-rw-r--r--handler/Contacts.php16
-rw-r--r--handler/Index.php20
-rw-r--r--handler/PostId.php20
-rw-r--r--handler/ProjectsHtml.php11
-rw-r--r--handler/RSS.php32
-rw-r--r--handler/admin/AdminRequestHandler.php20
-rw-r--r--handler/admin/AutoAddOrEdit.php97
-rw-r--r--handler/admin/AutoDelete.php34
-rw-r--r--handler/admin/AutoEdit.php127
-rw-r--r--handler/admin/Index.php13
-rw-r--r--handler/admin/Login.php31
-rw-r--r--handler/admin/Logout.php17
-rw-r--r--handler/admin/MarkdownPreview.php22
-rw-r--r--handler/admin/PageAdd.php66
-rw-r--r--handler/admin/PostAdd.php68
-rw-r--r--handler/admin/UploadDelete.php25
-rw-r--r--handler/admin/UploadEditNote.php25
-rw-r--r--handler/admin/Uploads.php73
19 files changed, 823 insertions, 0 deletions
diff --git a/handler/Auto.php b/handler/Auto.php
new file mode 100644
index 0000000..c0d4c13
--- /dev/null
+++ b/handler/Auto.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace handler;
+
+use admin;
+use NotFoundException;
+use pages;
+use Post;
+use posts;
+use RedirectResponse;
+use RequestHandler;
+use Response;
+use Tag;
+
+class Auto extends RequestHandler {
+
+ public function get(): Response {
+ list($name) = $this->input('name');
+ if ($name == 'coreboot-mba51-flashing')
+ return new RedirectResponse('/coreboot-mba52-flashing/');
+
+ if (is_numeric($name)) {
+ $post = posts::get((int)$name);
+ } else {
+ $post = posts::getPostByName($name);
+ }
+ if ($post)
+ return $this->getPost($post);
+
+ $tag = posts::getTag($name);
+ if ($tag)
+ return $this->getTag($tag);
+
+ $page = pages::getPageByName($name);
+ if ($page)
+ return $this->getPage($page);
+
+ if (admin::isAdmin()) {
+ $this->skin->title = $name;
+ return $this->skin->renderPage('admin/pageNew',
+ short_name: $name);
+ }
+
+ throw new NotFoundException();
+ }
+
+ public function getPost(Post $post): Response {
+ global $config;
+
+ if (!$post->visible && !admin::isAdmin())
+ throw new NotFoundException();
+
+ $tags = $post->getTags();
+
+ $s = $this->skin;
+ $s->meta[] = ['property' => 'og:title', 'content' => $post->title];
+ $s->meta[] = ['property' => 'og:url', 'content' => fullURL($post->getUrl())];
+ if (($img = $post->getFirstImage()) !== null)
+ $s->meta[] = ['property' => 'og:image', 'content' => $img->getDirectUrl()];
+ $s->meta[] = [
+ 'name' => 'description',
+ 'property' => 'og:description',
+ 'content' => $post->getDescriptionPreview(155)
+ ];
+
+ $s->title = $post->title;
+
+ return $s->renderPage('main/post',
+ title: $post->title,
+ id: $post->id,
+ unsafe_html: $post->getHtml($this->isRetina()),
+ date: $post->getFullDate(),
+ tags: $tags,
+ visible: $post->visible,
+ url: $post->getUrl(),
+ email: $config['admin_email'],
+ urlencoded_reply_subject: 'Re: '.$post->title);
+ }
+
+ public function getTag(Tag $tag): Response {
+ $tag = posts::getTag($tag);
+ if (!admin::isAdmin() && !$tag->visiblePostsCount)
+ throw new NotFoundException();
+
+ $count = posts::getPostsCountByTagId($tag->id, admin::isAdmin());
+ $posts = $count ? posts::getPostsByTagId($tag->id, admin::isAdmin()) : [];
+
+ $this->skin->title = '#'.$tag->tag;
+ return $this->skin->renderPage('main/tag',
+ count: $count,
+ posts: $posts,
+ tag: $tag->tag);
+ }
+
+ public function getPage(\Page $page): Response {
+ if (!admin::isAdmin() && !$page->visible)
+ throw new NotFoundException();
+
+ $this->skin->title = $page ? $page->title : '???';
+ return $this->skin->renderPage('main/page',
+ unsafe_html: $page->getHtml($this->isRetina()),
+ page_url: $page->getUrl(),
+ short_name: $page->shortName);
+ }
+
+} \ No newline at end of file
diff --git a/handler/Contacts.php b/handler/Contacts.php
new file mode 100644
index 0000000..c60479d
--- /dev/null
+++ b/handler/Contacts.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace handler;
+
+use Response;
+
+class Contacts extends \RequestHandler {
+
+ public function get(): Response {
+ global $config;
+ $this->skin->title = $this->lang['contacts'];
+ return $this->skin->renderPage('main/contacts',
+ email: $config['admin_email']);
+ }
+
+} \ No newline at end of file
diff --git a/handler/Index.php b/handler/Index.php
new file mode 100644
index 0000000..c852511
--- /dev/null
+++ b/handler/Index.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace handler;
+
+use admin;
+use posts;
+
+class Index extends \RequestHandler {
+
+ public function get(): \Response {
+ $posts = posts::getPosts(include_hidden: admin::isAdmin());
+ $tags = posts::getAllTags(include_hidden: admin::isAdmin());
+
+ $this->skin->title = "ch1p's Blog";
+ $this->skin->setOptions(['dynlogo_enabled' => false]);
+ return $this->skin->renderPage('main/index',
+ posts: $posts,
+ tags: $tags);
+ }
+} \ No newline at end of file
diff --git a/handler/PostId.php b/handler/PostId.php
new file mode 100644
index 0000000..ec9f750
--- /dev/null
+++ b/handler/PostId.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace handler;
+
+class PostId extends \RequestHandler {
+
+ public function get(): \Response {
+ list($post_id) = $this->input('i:id');
+
+ $post = posts_getPost($post_id);
+ if (!$post || (!$post->visible && !\admin::isAdmin()))
+ throw new \NotFoundException();
+
+ if ($post->shortName != '')
+ return new \RedirectResponse($post->getUrl());
+
+ throw new \NotFoundException();
+ }
+
+} \ No newline at end of file
diff --git a/handler/ProjectsHtml.php b/handler/ProjectsHtml.php
new file mode 100644
index 0000000..beada44
--- /dev/null
+++ b/handler/ProjectsHtml.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace handler\main;
+
+class ProjectsHtml extends \RequestHandler {
+
+ public function get(): \Response {
+ return new \RedirectResponse('/projects/');
+ }
+
+} \ No newline at end of file
diff --git a/handler/RSS.php b/handler/RSS.php
new file mode 100644
index 0000000..08a2136
--- /dev/null
+++ b/handler/RSS.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace handler;
+use posts;
+use Response;
+use SkinContext;
+
+class RSS extends \RequestHandler {
+
+ public function get(): Response {
+ global $config;
+
+ $items = array_map(fn(\Post $post) => [
+ 'title' => $post->title,
+ 'link' => $post->getUrl(),
+ 'pub_date' => date(DATE_RSS, $post->ts),
+ 'description' => $post->getDescriptionPreview(500),
+ ], posts::getPosts(0, 20));
+
+ $ctx = new SkinContext('\\skin\\rss');
+ $body = $ctx->atom(
+ title: ($this->lang)('site_title'),
+ link: 'https://'.$config['domain'],
+ rss_link: 'https://'.$config['domain'].'/feed.rss',
+ items: $items);
+
+ $response = new Response(200, $body);
+ $response->addHeader('Content-Type: application/rss+xml; charset=utf-8');
+ return $response;
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/AdminRequestHandler.php b/handler/admin/AdminRequestHandler.php
new file mode 100644
index 0000000..04b7cde
--- /dev/null
+++ b/handler/admin/AdminRequestHandler.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace handler\admin;
+
+use admin;
+use Response;
+
+class AdminRequestHandler extends \RequestHandler {
+
+ public function beforeDispatch(): ?Response {
+ $this->skin->static[] = '/css/admin.css';
+ $this->skin->static[] = '/js/admin.js';
+
+ if (!($this instanceof Login) && !admin::isAdmin())
+ throw new \ForbiddenException('looks like you are not admin');
+
+ return null;
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/AutoAddOrEdit.php b/handler/admin/AutoAddOrEdit.php
new file mode 100644
index 0000000..027c827
--- /dev/null
+++ b/handler/admin/AutoAddOrEdit.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace handler\admin;
+
+use Page;
+use Post;
+use Response;
+
+abstract class AutoAddOrEdit extends AdminRequestHandler {
+
+ public function beforeDispatch(): ?Response {
+ $this->skin->setOptions([
+ 'full_width' => true,
+ 'no_footer' => true
+ ]);
+ return parent::beforeDispatch();
+ }
+
+ protected function _get_postAdd(
+ string $title = '',
+ string $text = '',
+ ?array $tags = null,
+ string $short_name = '',
+ ?string $error_code = null
+ ): Response {
+ $this->skin->addLangKeys($this->lang->search('/^(err_)?blog_/'));
+ $this->skin->title = $this->lang['blog_write'];
+ return $this->skin->renderPage('admin/postForm',
+ title: $title,
+ text: $text,
+ tags: $tags ? implode(', ', $tags) : '',
+ short_name: $short_name,
+ error_code: $error_code);
+ }
+
+ protected function _get_postEdit(
+ Post $post,
+ string $title = '',
+ string $text = '',
+ ?array $tags = null,
+ bool $visible = false,
+ string $short_name = '',
+ ?string $error_code = null,
+ bool $saved = false,
+ ): Response {
+ $this->skin->addLangKeys($this->lang->search('/^(err_)?blog_/'));
+ $this->skin->title = ($this->lang)('blog_post_edit_title', $post->title);
+ return $this->skin->renderPage('admin/postForm',
+ is_edit: true,
+ post_id: $post->id,
+ post_url: $post->getUrl(),
+ title: $title,
+ text: $text,
+ tags: $tags ? implode(', ', $tags) : '',
+ visible: $visible,
+ saved: $saved,
+ short_name: $short_name,
+ error_code: $error_code
+ );
+ }
+
+ protected function _get_pageAdd(
+ string $name,
+ string $title = '',
+ string $text = '',
+ ?string $error_code = null
+ ): Response {
+ $this->skin->addLangKeys($this->lang->search('/^(err_)?pages_/'));
+ $this->skin->title = ($this->lang)('pages_create_title', $name);
+ return $this->skin->renderPage('admin/pageForm',
+ short_name: $name,
+ title: $title,
+ text: $text,
+ error_code: $error_code);
+ }
+
+ protected function _get_pageEdit(
+ Page $page,
+ string $title = '',
+ string $text = '',
+ bool $saved = false,
+ bool $visible = false,
+ ?string $error_code = null
+ ): Response {
+ $this->skin->addLangKeys($this->lang->search('/^(err_)?pages_/'));
+ $this->skin->title = ($this->lang)('pages_page_edit_title', $page->shortName.'.html');
+ return $this->skin->renderPage('admin/pageForm',
+ is_edit: true,
+ short_name: $page->shortName,
+ title: $title,
+ text: $text,
+ visible: $visible,
+ saved: $saved,
+ error_code: $error_code);
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/AutoDelete.php b/handler/admin/AutoDelete.php
new file mode 100644
index 0000000..80c8eef
--- /dev/null
+++ b/handler/admin/AutoDelete.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace handler\admin;
+
+use csrf;
+use NotFoundException;
+use pages;
+use posts;
+use RedirectResponse;
+use Response;
+
+class AutoDelete extends AdminRequestHandler {
+
+ public function get(): Response {
+ list($name) = $this->input('short_name');
+
+ $post = posts::getPostByName($name);
+ if ($post) {
+ csrf::check('delpost'.$post->id);
+ posts::delete($post);
+ return new RedirectResponse('/');
+ }
+
+ $page = pages::getPageByName($name);
+ if ($page) {
+ csrf::check('delpage'.$page->shortName);
+ pages::delete($page);
+ return new RedirectResponse('/');
+ }
+
+ throw new NotFoundException();
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/AutoEdit.php b/handler/admin/AutoEdit.php
new file mode 100644
index 0000000..9d70c5b
--- /dev/null
+++ b/handler/admin/AutoEdit.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace handler\admin;
+
+use csrf;
+use pages;
+use posts;
+use Response;
+
+class AutoEdit extends AutoAddOrEdit {
+
+ public function get(): Response {
+ list($short_name, $saved) = $this->input('short_name, b:saved');
+
+ $post = posts::getPostByName($short_name);
+ if ($post) {
+ $tags = $post->getTags();
+ return $this->_get_postEdit($post,
+ tags: $post->getTags(),
+ saved: $saved,
+ title: $post->title,
+ text: $post->md,
+ visible: $post->visible,
+ short_name: $post->shortName,
+ );
+ }
+
+ $page = pages::getPageByName($short_name);
+ if ($page) {
+ return $this->_get_pageEdit($page,
+ title: $page->title,
+ text: $page->md,
+ visible: $page->visible,
+ saved: $saved,
+ );
+ }
+
+ throw new \NotFoundException();
+ }
+
+ public function post(): Response {
+ list($short_name) = $this->input('short_name');
+
+ $post = posts::getPostByName($short_name);
+ if ($post) {
+ csrf::check('editpost'.$post->id);
+
+ list($text, $title, $tags, $visible, $short_name)
+ = $this->input('text, title, tags, b:visible, new_short_name');
+
+ $tags = posts::splitStringToTags($tags);
+ $error_code = null;
+
+ if (!$title) {
+ $error_code = 'no_title';
+ } else if (!$text) {
+ $error_code = 'no_text';
+ } else if (empty($tags)) {
+ $error_code = 'no_tags';
+ } else if (empty($short_name)) {
+ $error_code = 'no_short_name';
+ }
+
+ if ($error_code)
+ $this->_get_postEdit($post,
+ text: $text,
+ title: $title,
+ tags: $tags,
+ visible: $visible,
+ short_name: $short_name,
+ error_code: $error_code
+ );
+
+ $post->edit([
+ 'title' => $title,
+ 'md' => $text,
+ 'visible' => (int)$visible,
+ 'short_name' => $short_name
+ ]);
+ $tag_ids = posts::getTagIds($tags);
+ $post->setTagIds($tag_ids);
+
+ return new \RedirectResponse($post->getUrl().'edit/?saved=1');
+ }
+
+ $page = pages::getPageByName($short_name);
+ if ($page) {
+ csrf::check('editpage'.$page->shortName);
+
+ list($text, $title, $visible, $short_name)
+ = $this->input('text, title, b:visible, new_short_name');
+
+ $text = trim($text);
+ $title = trim($title);
+ $error_code = null;
+
+ if (!$title) {
+ $error_code = 'no_title';
+ } else if (!$text) {
+ $error_code = 'no_text';
+ } else if (!$short_name) {
+ $error_code = 'no_short_name';
+ }
+
+ if ($error_code) {
+ return $this->_get_pageEdit($page,
+ title: $title,
+ text: $text,
+ visible: $visible,
+ error_code: $error_code
+ );
+ }
+
+ $page->edit([
+ 'title' => $title,
+ 'md' => $text,
+ 'visible' => (int)$visible,
+ 'short_name' => $short_name,
+ ]);
+
+ return new \RedirectResponse($page->getUrl().'edit/?saved=1');
+ }
+
+ throw new \NotFoundException();
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/Index.php b/handler/admin/Index.php
new file mode 100644
index 0000000..e829913
--- /dev/null
+++ b/handler/admin/Index.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace handler\admin;
+
+use Response;
+
+class Index extends AdminRequestHandler {
+
+ public function get(): Response {
+ return $this->skin->renderPage('admin/index');
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/Login.php b/handler/admin/Login.php
new file mode 100644
index 0000000..cade137
--- /dev/null
+++ b/handler/admin/Login.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace handler\admin;
+
+use admin;
+use csrf;
+use RedirectResponse;
+use Response;
+use UnauthorizedException;
+
+class Login extends AdminRequestHandler {
+
+ public function get(): Response {
+ if (admin::isAdmin())
+ return new RedirectResponse('/admin/');
+ return $this->skin->renderPage('admin/login');
+ }
+
+ public function post(): Response {
+ csrf::check('adminlogin');
+ $password = $_POST['password'] ?? '';
+ $valid = admin::checkPassword($password);
+ if ($valid) {
+ admin::logAuth();
+ admin::setCookie();
+ return new RedirectResponse('/admin/');
+ }
+ throw new UnauthorizedException('nice try');
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/Logout.php b/handler/admin/Logout.php
new file mode 100644
index 0000000..bb11e43
--- /dev/null
+++ b/handler/admin/Logout.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace handler\admin;
+
+use admin;
+use csrf;
+use Response;
+
+class Logout extends AdminRequestHandler {
+
+ public function get(): Response {
+ csrf::check('logout');
+ admin::unsetCookie();
+ return new \RedirectResponse('/admin/login/');
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/MarkdownPreview.php b/handler/admin/MarkdownPreview.php
new file mode 100644
index 0000000..e513709
--- /dev/null
+++ b/handler/admin/MarkdownPreview.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace handler\admin;
+
+use Response;
+
+class MarkdownPreview extends AdminRequestHandler {
+
+ public function post(): Response {
+ list($md, $title, $use_image_previews) = $this->input('md, title, b:use_image_previews');
+
+ $html = \markup::markdownToHtml($md, $use_image_previews);
+
+ $ctx = new \SkinContext('\\skin\\admin');
+ $html = $ctx->markdownPreview(
+ unsafe_html: $html,
+ title: $title
+ );
+ return new \AjaxOkResponse(['html' => $html]);
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/PageAdd.php b/handler/admin/PageAdd.php
new file mode 100644
index 0000000..8754f0f
--- /dev/null
+++ b/handler/admin/PageAdd.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace handler\admin;
+
+use csrf;
+use NotFoundException;
+use pages;
+use RedirectResponse;
+use Response;
+
+class PageAdd extends AutoAddOrEdit {
+
+ public function get(): Response {
+ list($name) = $this->input('short_name');
+ $page = pages::getPageByName($name);
+ if ($page)
+ throw new NotFoundException();
+
+ return $this->_get_pageAdd($name);
+ }
+
+ public function post(): Response {
+ csrf::check('addpage');
+
+ list($name) = $this->input('short_name');
+ $page = pages::getPageByName($name);
+ if ($page)
+ throw new NotFoundException();
+
+ $text = trim($_POST['text'] ?? '');
+ $title = trim($_POST['title'] ?? '');
+ $error_code = null;
+
+ if (!$title) {
+ $error_code = 'no_title';
+ } else if (!$text) {
+ $error_code = 'no_text';
+ }
+
+ if ($error_code) {
+ return $this->_get_pageAdd(
+ name: $name,
+ text: $text,
+ title: $title,
+ error_code: $error_code
+ );
+ }
+
+ if (!pages::add([
+ 'short_name' => $name,
+ 'title' => $title,
+ 'md' => $text
+ ])) {
+ return $this->_get_pageAdd(
+ name: $name,
+ text: $text,
+ title: $title,
+ error_code: 'db_err'
+ );
+ }
+
+ $page = pages::getPageByName($name);
+ return new RedirectResponse($page->getUrl());
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/PostAdd.php b/handler/admin/PostAdd.php
new file mode 100644
index 0000000..c21a239
--- /dev/null
+++ b/handler/admin/PostAdd.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace handler\admin;
+
+use csrf;
+use posts;
+use RedirectResponse;
+use Response;
+
+class PostAdd extends AutoAddOrEdit {
+
+ public function get(): Response {
+ return $this->_get_postAdd();
+ }
+
+ public function post(): Response {
+ csrf::check('addpost');
+
+ list($text, $title, $tags, $visible, $short_name)
+ = $this->input('text, title, tags, b:visible, short_name');
+ $tags = posts::splitStringToTags($tags);
+
+ $error_code = null;
+
+ if (!$title) {
+ $error_code = 'no_title';
+ } else if (!$text) {
+ $error_code = 'no_text';
+ } else if (empty($tags)) {
+ $error_code = 'no_tags';
+ } else if (empty($short_name)) {
+ $error_code = 'no_short_name';
+ }
+
+ if ($error_code)
+ return $this->_get_postAdd(
+ text: $text,
+ title: $title,
+ tags: $tags,
+ short_name: $short_name,
+ error_code: $error_code
+ );
+
+ $id = posts::add([
+ 'title' => $title,
+ 'md' => $text,
+ 'visible' => (int)$visible,
+ 'short_name' => $short_name,
+ ]);
+
+ if (!$id)
+ $this->_get_postAdd(
+ text: $text,
+ title: $title,
+ tags: $tags,
+ short_name: $short_name,
+ error_code: 'db_err'
+ );
+
+ // set tags
+ $post = posts::get($id);
+ $tag_ids = posts::getTagIds($tags);
+ $post->setTagIds($tag_ids);
+
+ return new RedirectResponse($post->getUrl());
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/UploadDelete.php b/handler/admin/UploadDelete.php
new file mode 100644
index 0000000..26b58b7
--- /dev/null
+++ b/handler/admin/UploadDelete.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace handler\admin;
+
+use csrf;
+use RedirectResponse;
+use Response;
+
+class UploadDelete extends AdminRequestHandler {
+
+ public function get(): Response {
+ list($id) = $this->input('i:id');
+
+ $upload = \uploads::get($id);
+ if (!$upload)
+ return new RedirectResponse('/uploads/?error='.urlencode('upload not found'));
+
+ csrf::check('delupl'.$id);
+
+ \uploads::delete($id);
+
+ return new RedirectResponse('/uploads/');
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/UploadEditNote.php b/handler/admin/UploadEditNote.php
new file mode 100644
index 0000000..e7cdbb2
--- /dev/null
+++ b/handler/admin/UploadEditNote.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace handler\admin;
+
+use csrf;
+use Response;
+
+class UploadEditNote extends AdminRequestHandler {
+
+ public function post(): Response {
+ list($id) = $this->input('i:id');
+
+ $upload = \uploads::get($id);
+ if (!$upload)
+ return new \RedirectResponse('/uploads/?error='.urlencode('upload not found'));
+
+ csrf::check('editupl'.$id);
+
+ $note = $_POST['note'] ?? '';
+ $upload->setNote($note);
+
+ return new \RedirectResponse('/uploads/');
+ }
+
+} \ No newline at end of file
diff --git a/handler/admin/Uploads.php b/handler/admin/Uploads.php
new file mode 100644
index 0000000..0cbb2f6
--- /dev/null
+++ b/handler/admin/Uploads.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace handler\admin;
+
+use csrf;
+use RedirectResponse;
+use Response;
+
+// So it's 2022 outside, and it's PHP 8.1 already, which is actually so cool comparing to 5.x and even 7.4, but...
+// ...class names are still case-insensitive?!! And I can't import \uploads because it's the same as Uploads?!!
+//
+// PHP, what the fuck is wrong with you?!
+
+class Uploads extends AdminRequestHandler {
+
+ public function get(): Response {
+ list($error) = $this->input('error');
+ $uploads = \uploads::getAll();
+
+ $this->skin->title = ($this->lang)('blog_upload');
+ return $this->skin->renderPage('admin/uploads',
+ error: $error,
+ uploads: $uploads);
+ }
+
+ public function post(): Response {
+ csrf::check('addupl');
+
+ list($custom_name, $note) = $this->input('name, note');
+
+ if (!isset($_FILES['files']))
+ return new RedirectResponse('/uploads/?error='.urlencode('no file'));
+
+ $files = [];
+ for ($i = 0; $i < count($_FILES['files']['name']); $i++) {
+ $files[] = [
+ 'name' => $_FILES['files']['name'][$i],
+ 'type' => $_FILES['files']['type'][$i],
+ 'tmp_name' => $_FILES['files']['tmp_name'][$i],
+ 'error' => $_FILES['files']['error'][$i],
+ 'size' => $_FILES['files']['size'][$i],
+ ];
+ }
+
+ if (count($files) > 1) {
+ $note = '';
+ $custom_name = '';
+ }
+
+ foreach ($files as $f) {
+ if ($f['error'])
+ return new RedirectResponse('/uploads/?error='.urlencode('error code '.$f['error']));
+
+ if (!$f['size'])
+ return new RedirectResponse('/uploads/?error='.urlencode('received empty file'));
+
+ $ext = extension($f['name']);
+ if (!\uploads::isExtensionAllowed($ext))
+ return new RedirectResponse('/uploads/?error='.urlencode('extension not allowed'));
+
+ $upload_id = \uploads::add(
+ $f['tmp_name'],
+ $custom_name ?: $f['name'],
+ $note);
+
+ if (!$upload_id)
+ return new RedirectResponse('/uploads/?error='.urlencode('failed to create upload'));
+ }
+
+ return new RedirectResponse('/uploads/');
+ }
+
+} \ No newline at end of file