diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..16a9eb0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,8 @@
+.git
+.github
+.vscode
+app/react/node_modules
+app/public/content
+vendor
+*.log
+.DS_Store
diff --git a/.gitignore b/.gitignore
index 0d64a3c..f68b37a 100755
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,9 @@
*.log
composer.lock
app/database/db.sqlite
-public/content/*
-public/assets/js/tinymce
+app/public/content/*
+app/public/assets/js/tinymce
+app/public/assets/js/admin.*
+app/react/node_modules
+app/react/package-lock.json
vendor
\ No newline at end of file
diff --git a/.htaccess b/.htaccess
index 3e45d8a..6ab68af 100755
--- a/.htaccess
+++ b/.htaccess
@@ -1,5 +1,6 @@
Options -Indexes
RewriteEngine on
-RewriteRule ^$ public/ [L]
-RewriteRule ^((?!public/).*)$ public/$1 [L,NC]
+RewriteRule ^$ app/public/ [L]
+RewriteRule ^public/(.*)$ app/public/$1 [L,NC]
+RewriteRule ^((?!app/public/).*)$ app/public/$1 [L,NC]
diff --git a/README.md b/README.md
index e7a37ba..744e2e3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
Aurora
diff --git a/app/bootstrap/config.php b/app/bootstrap/config.php
index a399fe7..e25b142 100644
--- a/app/bootstrap/config.php
+++ b/app/bootstrap/config.php
@@ -1,6 +1,6 @@
$db,
'content' => 'public/content',
'per_page' => 20,
+ 'max_active_sessions' => 10,
'mail' => fn($to, $subject, $message) => mail($to, $subject, $message),
- 'views' => 'app/views',
+ 'views' => 'views',
];
diff --git a/app/bootstrap/index.php b/app/bootstrap/index.php
index 8076e98..7e64012 100644
--- a/app/bootstrap/index.php
+++ b/app/bootstrap/index.php
@@ -26,20 +26,14 @@ function setting(?string $key = null): mixed
$settings = $db->query('SELECT `key`, value FROM settings')->fetchAll(\PDO::FETCH_KEY_PAIR);
header('X-Content-Type-Options: nosniff');
- ini_set('session.cookie_httponly', 1);
ini_set('error_log', \Aurora\Core\Helper::getPath($settings['log_file']));
- ini_set('display_errors', $settings['display_errors'] ? 1 : 0);
- ini_set('display_startup_errors', $settings['display_errors'] ? 1 : 0);
+ ini_set('display_errors', 0);
+ ini_set('display_startup_errors', 0);
error_reporting($settings['log_errors'] ? E_ALL : 0);
date_default_timezone_set($settings['timezone']);
- session_set_cookie_params([
- 'lifetime' => (int) $settings['session_lifetime'],
- 'samesite' => $settings['samesite_cookie'],
- ]);
- session_start();
$languages = [];
- foreach (glob(\Aurora\Core\Helper::getPath('app/languages/*.php')) as $file) {
+ foreach (glob(\Aurora\Core\Helper::getPath('languages/*.php')) as $file) {
$languages[pathinfo($file, PATHINFO_FILENAME)] = require_once($file);
}
@@ -47,11 +41,13 @@ function setting(?string $key = null): mixed
$view = new \Aurora\Core\View(\Aurora\Core\Helper::getPath($kernel->config('views')), new \Aurora\App\ViewHelper($kernel->config('date_format'), $lang));
- \Aurora\App\Permission::set($db->query('SELECT permission, role_level FROM roles_permissions ORDER BY permission')->fetchAll(\PDO::FETCH_KEY_PAIR), $_SESSION['user']['role'] ?? 0);
- \Aurora\App\Permission::addMethod('impersonate', fn($user) => ($user['status'] ?? false) && $user['role'] <= ($_SESSION['user']['role'] ?? 0) && \Aurora\App\Permission::can('impersonate'));
- \Aurora\App\Permission::addMethod('edit_user', fn($user) => ($user['role'] ?? 0) <= ($_SESSION['user']['role'] ?? 0) && \Aurora\App\Permission::can('edit_users'));
+ $user = &$GLOBALS['user'];
+ \Aurora\App\Permission::set($db->query('SELECT permission, role_level FROM roles_permissions ORDER BY permission')->fetchAll(\PDO::FETCH_KEY_PAIR), $user['role'] ?? 0);
+ \Aurora\App\Permission::addMethod('editUser', function ($subject) use (&$user) {
+ return \Aurora\App\Modules\User::canEdit($user, $subject);
+ });
\Aurora\App\Setting::set($settings);
\Aurora\App\Media::setDirectory($kernel->config('content'));
- (require('routes.php'))($kernel, $db, $view, $lang);
+ (require('routes.php'))($kernel, $db, $view, $lang, $user);
};
diff --git a/app/bootstrap/routes.php b/app/bootstrap/routes.php
index bf7301a..4abb587 100644
--- a/app/bootstrap/routes.php
+++ b/app/bootstrap/routes.php
@@ -2,7 +2,7 @@
use Aurora\Core\{DB, Helper, Kernel, Language, View};
-return function (\Aurora\Core\Kernel $kernel, DB $db, View $view, Language $lang) {
+return function (\Aurora\Core\Kernel $kernel, DB $db, View $view, Language $lang, ?array &$user) {
$user_mod = new \Aurora\App\Modules\User($db, $lang);
$tag_mod = new \Aurora\App\Modules\Tag($db, $lang);
$link_mod = new \Aurora\App\Modules\Link($db, $lang);
@@ -14,871 +14,605 @@
$rss = \Aurora\App\Setting::get('rss');
$router = $kernel->router;
- $router->middleware('*', function() use ($db, $view, $lang, $theme_dir) {
- if (Helper::isValidId($_SESSION['user']['id'] ?? false)) {
- $_SESSION['user'] = $db->query('SELECT * FROM users WHERE id = ? AND status', $_SESSION['user']['id'])->fetch();
- }
-
- if (\Aurora\App\Setting::get('maintenance') && !str_starts_with(Helper::getCurrentPath(), 'admin') && !Helper::isValidId($_SESSION['user']['id'] ?? false)) {
- echo $view->get("$theme_dir/information.html", [
- 'description' => $lang->get('under_maintenance'),
- 'subdescription' => $lang->get('come_back_soon'),
+ $getAuthToken = function() {
+ return preg_match('/Bearer\s(\S+)/i', Helper::getAuthorizationHeader(), $matches)
+ ? $matches[1]
+ : ($_COOKIE['auth_token'] ?? false);
+ };
+
+ $setAuthToken = function($token, $time) {
+ return setcookie('auth_token',
+ $token,
+ [
+ 'expires' => $time,
+ 'path' => '/',
+ 'domain' => '',
+ 'secure' => \Aurora\Core\Helper::isHttps(),
+ 'httponly' => true,
+ 'samesite' => 'Lax',
]);
- exit;
- }
+ };
- if ($_SERVER['REQUEST_METHOD'] === 'POST' && !Helper::isCsrfTokenValid($_POST['csrf'] ?? '')) {
- echo json_encode([ 'reload' => true ]);
- exit;
- }
- });
-
- $router->code(404, function() use ($view, $lang, $theme_dir) {
- return $view->get("$theme_dir/information.html", [
- 'title' => '404',
- 'description' => $lang->get('not_found'),
- 'subdescription' => $lang->get('not_found_desc'),
- ]);
+ $router->get([ 'admin', 'admin/*' ], function() use ($view) {
+ return $view->get('admin.html');
});
/**
- * ADMIN
+ * BLOG
*/
- $router->middleware('admin/*', function() use ($db) {
- if ((!Helper::isValidId($_SESSION['user']['id'] ?? false) || !($_SESSION['user']['status'] ?? false)) &&
- !in_array(Helper::getCurrentPath(), [ 'admin', 'admin/login', 'admin/send_password_restore', 'admin/new_password', 'admin/password_restore' ])) {
- header('Location: /admin');
- exit;
- }
+ $router->get('json:api/blog/posts', function() use ($view, $post_mod, $theme_dir) {
+ $current_page = max(1, (int) ($_GET['page'] ?? 1));
+ $per_page = \Aurora\App\Setting::get('per_page');
+ $where = [ $post_mod->getCondition([ 'status' => 1 ]) ];
- if (Helper::isValidId($_SESSION['user']['id'] ?? false)) {
- $db->update('users', [ 'last_active' => time() ], $_SESSION['user']['id']);
+ if (!empty($_GET['user'])) {
+ $where[] = 'posts.user_id = ' . ((int) $_GET['user']);
}
- });
- $router->get('admin', function() use ($view) {
- if (Helper::isValidId($_SESSION['user']['id'] ?? false)) {
- header('Location: /admin/dashboard');
+ if (!empty($_GET['tag'])) {
+ $where[] = 'posts.id IN (SELECT post_id FROM posts_to_tags WHERE tag_id = ' . ((int) $_GET['tag']) . ')';
}
- return $view->get('admin/login.html');
- });
-
- $router->post('json:admin/login', function() use ($user_mod) {
- $errors = $user_mod->handleLogin($_POST['email'], $_POST['password']);
+ $where = implode(' AND ', $where);
return json_encode([
- 'success' => empty($errors),
- 'msg' => null,
- 'errors' => $errors,
+ 'next_page' => $post_mod->isNextPageAvailable($current_page, $per_page, $where),
+ 'html' => $view->get("$theme_dir/partials/posts_page.html", [
+ 'posts' => $post_mod->getPage($current_page, $per_page, $where),
+ ]),
]);
});
- $router->post('json:admin/send_password_restore', function() use ($view, $user_mod) {
- $hash = bin2hex(random_bytes(18));
- $errors = $user_mod->requestPasswordRestore($_POST['email'],
- $hash,
- $view->get('admin/emails/password_restore.html', [ 'hash' => $hash ]));
-
- return json_encode([
- 'success' => empty($errors),
- 'errors' => $errors,
- ]);
- });
+ $router->get($blog_url, function() use ($db, $view, $lang, $link_mod, $post_mod, $theme_dir) {
+ $current_page = max(1, (int) ($_GET['page'] ?? 1));
+ $per_page = \Aurora\App\Setting::get('per_page');
+ $where = $post_mod->getCondition([ 'status' => 1 ]);
+ $search = $db->escape($_GET['search'] ?? '');
- $router->get('admin/new_password', function() use ($view) {
- return $view->get('admin/password_restore.html', [ 'hash' => $_GET['hash'] ]);
- });
+ if (!empty($search)) {
+ $where .= " AND (posts.title LIKE '%$search%' OR posts.description LIKE '%$search%')";
+ }
- $router->post('json:admin/password_restore', function() use ($user_mod) {
- $error = $user_mod->passwordRestore($_POST['hash'], $_POST['password'], $_POST['password_confirm']);
- return json_encode([
- 'success' => empty($error),
- 'errors' => [ $error ],
+ return $view->get("$theme_dir/blog.html", [
+ 'header_links' => $link_mod->getHeaderLinks(),
+ 'title' => $lang->get('blog'),
+ 'posts' => $post_mod->getPage($current_page, $per_page, $where, 'date', false, true),
+ 'next_page' => $post_mod->isNextPageAvailable($current_page, $per_page, $where),
+ 'current_page' => $current_page,
]);
});
- $router->get('admin/logout', function() {
- session_destroy();
- header('Location: /admin');
- });
+ $router->get("$blog_url/author/{author}", function() use ($view, $user_mod, $link_mod, $post_mod, $theme_dir) {
+ $current_page = max(1, (int) ($_GET['page'] ?? 1));
+ $author = $user_mod->get([ 'slug' => $_GET['author'] ]);
+ $per_page = \Aurora\App\Setting::get('per_page');
- $router->get('admin/dashboard', function() use ($db, $view, $link_mod, $post_mod) {
- return $view->get('admin/dashboard.html', [
- 'links' => $link_mod->getPage(null, null, '', 'order'),
- 'posts' => $post_mod->getPage(1, 6, $post_mod->getCondition([ 'status' => 1 ]), 'published_at', false),
- 'total_posts' => $db->count('posts', '', $post_mod->getCondition([ 'status' => 1 ])),
- 'total_scheduled_posts' => $db->count('posts', '', $post_mod->getCondition([ 'status' => 'scheduled' ])),
- 'total_draft_posts' => $db->count('posts', '', 'status != 1'),
- 'total_pages' => $db->count('pages', '', 'status = 1'),
- 'total_draft_pages' => $db->count('pages', '', 'status != 1'),
- 'total_users' => $db->count('users', '', 'status = 1'),
- 'total_inactive_users' => $db->count('users', '', 'status != 1'),
+ if (!$author) {
+ return [ '', 404 ];
+ }
+
+ $where = implode(' AND ', [
+ $post_mod->getCondition([ 'status' => 1 ]),
+ 'users.id = ' . ((int) $author['id']),
]);
- });
- /* PAGES */
-
- $router->get('admin/pages', function() use ($view, $lang, $page_mod) {
- return $view->get('admin/list.html', [
- 'title' => $lang->get('pages'),
- 'show_add_button' => \Aurora\App\Permission::can('edit_pages'),
- 'columns' => [
- [ 'title' => '', 'class' => 'w100' ],
- [ 'title' => $lang->get('slug'), 'class' => 'w20' ],
- [ 'title' => $lang->get('edited'), 'class' => 'w20' ],
- [ 'title' => $lang->get('number_views'), 'class' => 'w10 numeric', 'condition' => \Aurora\App\Setting::get('views_count') ],
- [ 'title' => '', 'class' => 'w10 row-actions' ],
- ],
- 'extra_header' => 'admin/partials/extra_headers/pages.html',
- 'filters' => [
- 'status' => [
- 'title' => $lang->get('status'),
- 'options' => [
- '' => $lang->get('all'),
- '1' => $lang->get('published'),
- '0' => $lang->get('draft'),
- ],
- ],
- 'order' => [
- 'title' => $lang->get('sort_by'),
- 'options' => [
- 'title' => $lang->get('title'),
- 'status' => $lang->get('status'),
- 'slug' => $lang->get('slug'),
- 'edited' => $lang->get('edited'),
- 'views' => $lang->get('number_views'),
- ],
- ],
- 'sort' => [
- 'options' => [
- 'asc' => $lang->get('ascending'),
- 'desc' => $lang->get('descending'),
- ],
- ],
- ],
- 'defaults' => [
- 'order' => $page_mod::DEFAULT_ORDER,
- 'sort' => $page_mod::DEFAULT_SORT,
- ],
+ return $view->get("$theme_dir/blog.html", [
+ 'header_links' => $link_mod->getHeaderLinks(),
+ 'title' => $author['name'],
+ 'user' => $author,
+ 'posts' => $post_mod->getPage($current_page, $per_page, $where, 'date', false, true),
+ 'next_page' => $post_mod->isNextPageAvailable($current_page, $per_page, $where),
+ 'current_page' => $current_page,
]);
});
- $router->get('admin/pages/edit', function() use ($view, $link_mod, $page_mod, $theme_dir) {
- $page = Helper::isValidId($_GET['id'] ?? false) ? $page_mod->get([ 'id' => $_GET['id'] ]) : [];
- if (!$page && isset($_GET['id'])) {
- http_response_code(404);
- return;
- }
-
- $absolute_theme_dir = Helper::getPath(Kernel::config('views') . "/$theme_dir");
- $view_files = [];
+ $router->get("$blog_url/tag/{tag}", function() use ($view, $tag_mod, $link_mod, $post_mod, $theme_dir) {
+ $current_page = max(1, (int) ($_GET['page'] ?? 1));
+ $per_page = \Aurora\App\Setting::get('per_page');
+ $tag = $tag_mod->get([ 'slug' => $_GET['tag'] ]);
- foreach (Helper::getFileIterator($absolute_theme_dir) as $file) {
- if ($file->isFile()) {
- $view_files[] = mb_substr($file->getPathname(), mb_strlen($absolute_theme_dir) + 1);
- }
+ if (!$tag) {
+ return [ '', 404 ];
}
- natcasesort($view_files);
+ $where = implode(' AND ', [
+ $post_mod->getCondition([ 'status' => 1 ]),
+ 'posts.id IN (SELECT post_id FROM posts_to_tags WHERE tag_id = ' . ((int) $tag['id']) . ')',
+ ]);
- return $view->get('admin/page.html', [
+ return $view->get("$theme_dir/blog.html", [
'header_links' => $link_mod->getHeaderLinks(),
- 'page' => $page,
- 'view_files' => $view_files,
+ 'title' => $tag['name'],
+ 'tag' => $tag,
+ 'posts' => $post_mod->getPage($current_page, $per_page, $where, 'date', false, true),
+ 'next_page' => $post_mod->isNextPageAvailable($current_page, $per_page, $where),
+ 'current_page' => $current_page,
]);
});
- $router->post('json:admin/pages/remove', function() use ($lang, $page_mod) {
- if (!\Aurora\App\Permission::can('edit_pages')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
- }
-
- if (!$page_mod->remove(explode(',', $_POST['id']))) {
- http_response_code(500);
- return json_encode([ 'errors' => [ $lang->get('unexpected_error') ] ]);
- }
+ $router->get("$blog_url/{slug}", function() use ($db, $view, $link_mod, $post_mod, $theme_dir, &$user) {
+ $post_cond = $post_mod->getCondition([ 'status' => 1 ]);
- return json_encode([ 'success' => true ]);
- });
+ $post = $post_mod->get([
+ 'slug' => $_GET['slug'] ?? '',
+ empty($user) ? $post_cond : '',
+ ]);
- /* POSTS */
+ if (!$post) {
+ return [ '', 404 ];
+ }
- $router->get('admin/posts', function() use ($view, $lang, $post_mod, $user_mod) {
- $authors = [ '' => $lang->get('all') ];
- foreach ($user_mod->getPage() as $user) {
- $authors[$user['id']] = $user['name'];
+ if (\Aurora\App\Setting::get('views_count')) {
+ $db->replace('views', [
+ 'type' => 'post',
+ 'item_id' => $post['id'],
+ 'ip' => Helper::getUserIP(),
+ 'date' => time(),
+ ]);
}
- return $view->get('admin/list.html', [
- 'title' => $lang->get('posts'),
- 'show_add_button' => \Aurora\App\Permission::can('edit_posts'),
- 'columns' => [
- [ 'title' => '', 'class' => 'w100' ],
- [ 'title' => $lang->get('author'), 'class' => 'w20' ],
- [ 'title' => $lang->get('publish_date'), 'class' => 'w20' ],
- [ 'title' => $lang->get('number_views'), 'class' => 'w10 numeric', 'condition' => \Aurora\App\Setting::get('views_count') ],
- [ 'title' => '', 'class' => 'w10 row-actions' ],
- ],
- 'extra_header' => 'admin/partials/extra_headers/posts.html',
- 'filters' => [
- 'user' => [
- 'title' => $lang->get('author'),
- 'options' => $authors,
- ],
- 'status' => [
- 'title' => $lang->get('status'),
- 'options' => [
- '' => $lang->get('all'),
- '1' => $lang->get('published'),
- 'scheduled' => $lang->get('scheduled'),
- '0' => $lang->get('draft'),
- ],
- ],
- 'order' => [
- 'title' => $lang->get('sort_by'),
- 'options' => [
- 'title' => $lang->get('title'),
- 'author' => $lang->get('author'),
- 'date' => $lang->get('publish_date'),
- 'views' => $lang->get('number_views'),
- ],
- ],
- 'sort' => [
- 'options' => [
- 'asc' => $lang->get('ascending'),
- 'desc' => $lang->get('descending'),
- ],
- ],
- ],
- 'defaults' => [
- 'order' => $post_mod::DEFAULT_ORDER,
- 'sort' => $post_mod::DEFAULT_SORT,
- ],
+ return $view->get("$theme_dir/post.html", [
+ 'header_links' => $link_mod->getHeaderLinks(),
+ 'post' => $post,
+ 'related_posts' => empty($post['tags_id'])
+ ? []
+ : $post_mod->getPage(1, 3, "$post_cond AND p2t.tag_id IN (" . $post['tags_id'] . ') AND posts.id != ' . $post['id']),
]);
});
- $router->get('admin/posts/edit', function() use ($view, $user_mod, $tag_mod, $post_mod) {
- $post = Helper::isValidId($_GET['id'] ?? false) ? $post_mod->get([ 'id' => $_GET['id'] ]) : [];
- if (!$post && isset($_GET['id'])) {
- http_response_code(404);
- return;
- }
+ if (!empty($rss)) {
+ $router->get("xml:$rss", function() use ($post_mod, $view, $theme_dir) {
+ return $view->get("$theme_dir/rss.html", [
+ 'posts' => $post_mod->getPage(null, null, $post_mod->getCondition([ 'status' => 1 ]), 'date', false),
+ ]);
+ });
+ }
- return $view->get('admin/post.html', [
- 'users' => $user_mod->getPage(),
- 'tags' => $tag_mod->getPage(null, null, '', 'name'),
- 'post' => $post,
+ $router->get([ '/', '{slug}' ], function() use ($db, $view, $link_mod, $page_mod, $theme_dir, &$user) {
+ $page = $page_mod->get([
+ 'slug' => $_GET['slug'] ?? '',
+ empty($user) ? $page_mod->getCondition([ 'status' => 1 ]) : '',
]);
- });
- $router->post('json:admin/posts/remove', function() use ($lang, $post_mod) {
- if (!\Aurora\App\Permission::can('edit_posts')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ if (!$page) {
+ return [ '', 404 ];
}
- if (!$post_mod->remove(explode(',', $_POST['id']))) {
- http_response_code(500);
- return json_encode([ 'errors' => [ $lang->get('unexpected_error') ] ]);
+ if (\Aurora\App\Setting::get('views_count')) {
+ $db->replace('views', [
+ 'type' => 'page',
+ 'item_id' => $page['id'],
+ 'ip' => Helper::getUserIP(),
+ 'date' => time(),
+ ]);
}
- return json_encode([ 'success' => true ]);
- });
-
- $router->any('json:admin/posts/upload_image', function() {
- $path = Kernel::config('content') . '/' . date('Y/m/');
- \Aurora\App\Media::uploadFile($_FILES['file'], $path);
-
- return json_encode([ 'location' => "/$path/" . $_FILES['file']['name'] ]);
- });
-
- /* USERS */
+ $template = !empty($page['static']) && !empty($page['static_file'])
+ ? $page['static_file']
+ : 'page.html';
- $router->get('admin/users', function() use ($db, $view, $lang, $user_mod) {
- $roles = [ '' => $lang->get('all') ];
- foreach ($db->query('SELECT * FROM roles ORDER BY level ASC')->fetchAll() as $role) {
- $roles[$role['level']] = $lang->get($role['slug']);
- }
-
- return $view->get('admin/list.html', [
- 'title' => $lang->get('users'),
- 'show_add_button' => \Aurora\App\Permission::can('edit_users'),
- 'columns' => [
- [ 'title' => '', 'class' => 'w100' ],
- [ 'title' => $lang->get('role'), 'class' => 'w20' ],
- [ 'title' => $lang->get('last_active'), 'class' => 'w20' ],
- [ 'title' => $lang->get('number_posts'), 'class' => 'w10 numeric' ],
- [ 'title' => '', 'class' => 'w10 row-actions' ],
- ],
- 'extra_header' => 'admin/partials/extra_headers/users.html',
- 'filters' => [
- 'status' => [
- 'title' => $lang->get('status'),
- 'options' => [
- '' => $lang->get('all'),
- '1' => $lang->get('active'),
- '0' => $lang->get('inactive'),
- ],
- ],
- 'role' => [
- 'title' => $lang->get('role'),
- 'options' => $roles,
- ],
- 'order' => [
- 'title' => $lang->get('sort_by'),
- 'options' => [
- 'name' => $lang->get('name'),
- 'email' => $lang->get('email'),
- 'status' => $lang->get('status'),
- 'role' => $lang->get('role'),
- 'last_active' => $lang->get('last_active'),
- 'posts' => $lang->get('number_posts'),
- ],
- ],
- 'sort' => [
- 'options' => [
- 'asc' => $lang->get('ascending'),
- 'desc' => $lang->get('descending'),
- ],
- ],
- ],
- 'defaults' => [
- 'order' => $user_mod::DEFAULT_ORDER,
- 'sort' => $user_mod::DEFAULT_SORT,
- ],
+ return $view->get("$theme_dir/$template", [
+ 'header_links' => $link_mod->getHeaderLinks(),
+ ...$page,
]);
});
- $router->get('admin/users/edit', function() use ($db, $view, $user_mod) {
- $user = Helper::isValidId($_GET['id'] ?? false) ? $user_mod->get([ 'id' => $_GET['id'] ]) : [];
- if (!$user && isset($_GET['id'])) {
- http_response_code(404);
- return;
- }
-
- return $view->get('admin/user.html', [
- 'user' => $user,
- 'roles' => $db->query('SELECT * FROM roles ORDER BY level ASC')->fetchAll(),
- ]);
- });
+ $login = function($user_id) use ($db, $setAuthToken) {
+ $token = bin2hex(random_bytes(64));
- $router->post('json:admin/users/remove', function() use ($lang, $user_mod) {
- if (!\Aurora\App\Permission::can('edit_users')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ try {
+ $success = (bool) $db->insert('tokens', [
+ 'user_id' => $user_id,
+ 'token' => $token,
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ 'ip' => Helper::getUserIP(),
+ 'created_at' => time(),
+ 'updated_at' => time(),
+ ]);
+ } catch (\Exception) {
+ return [
+ 'success' => false,
+ 'error' => 'error_generic',
+ ];
}
- $ids = array_map(fn($id) => (int) $id, explode(',', $_POST['id']));
- $valid_ids = [];
-
- foreach ($user_mod->getPage(null, null, 'users.id IN (' . implode(',', $ids) . ')') as $user) {
- if (\Aurora\App\Permission::edit_user($user) && $user['id'] != $_SESSION['user']['id']) {
- $valid_ids[] = $user['id'];
+ if ($success) {
+ $total = (int) $db->query('SELECT COUNT(*) FROM tokens WHERE user_id = ?', $user_id)->fetchColumn();
+ $to_remove = (int) ($total - \Aurora\Core\Kernel::config('max_active_sessions'));
+ if ($to_remove > 0) {
+ $db->query('DELETE FROM tokens WHERE user_id = ? ORDER BY created_at ASC, token ASC LIMIT ?', $user_id, $to_remove);
}
- }
- if (!$user_mod->remove($valid_ids)) {
- http_response_code(500);
- return json_encode([ 'errors' => [ $lang->get('unexpected_error') ] ]);
+ $setAuthToken($token, time() + (60 * 60 * 24 * 30)); // 30 days
}
- return json_encode([ 'success' => true ]);
+ return [ 'success' => $success ];
+ };
+
+ /**
+ * NEW
+ */
+
+ $router->code(404, function() use ($view, $lang, $theme_dir) {
+ return $view->get("$theme_dir/information.html", [
+ 'title' => '404',
+ 'description' => $lang->get('not_found'),
+ 'subdescription' => $lang->get('not_found_desc'),
+ ]);
});
- $router->get('admin/users/impersonate', function() use ($user_mod) {
+ $router->middleware('*', function() use ($db, $view, $lang, $theme_dir, $user_mod, $getAuthToken, &$user) {
+ $token = $getAuthToken();
$user = $user_mod->get([
- 'id' => $_GET['id'] ?? 0,
+ 'id' => $db->query('SELECT user_id FROM tokens WHERE token = ?', $token)->fetchColumn(),
'status' => 1,
- ]);
+ ]) ?: null;
- if (!\Aurora\App\Permission::impersonate($user)) {
- http_response_code(403);
- return;
+ if ($user) {
+ $user['token'] = $token;
}
- $_SESSION['user'] = $user;
- header('Location: /admin/users');
+ \Aurora\App\Permission::set($db->query('SELECT permission, role_level FROM roles_permissions ORDER BY permission')->fetchAll(\PDO::FETCH_KEY_PAIR), $user['role'] ?? 0);
+
+ if (\Aurora\App\Setting::get('maintenance') && !str_starts_with(Helper::getCurrentPath(), 'admin') && !str_starts_with(Helper::getCurrentPath(), 'api') && !Helper::isValidId($user['id'] ?? false)) {
+ echo $view->get("$theme_dir/information.html", [
+ 'description' => $lang->get('under_maintenance'),
+ 'subdescription' => $lang->get('come_back_soon'),
+ ]);
+ exit;
+ }
});
- /* LINKS */
-
- $router->get('admin/links', function() use ($view, $lang, $link_mod) {
- return $view->get('admin/list.html', [
- 'title' => $lang->get('links'),
- 'show_add_button' => \Aurora\App\Permission::can('edit_links'),
- 'columns' => [
- [ 'title' => '', 'class' => 'w100' ],
- [ 'title' => $lang->get('url'), 'class' => 'w20' ],
- [ 'title' => $lang->get('status'), 'class' => 'w20' ],
- [ 'title' => $lang->get('order'), 'class' => 'w10 numeric' ],
- [ 'title' => '', 'class' => 'w10 row-actions' ],
- ],
- 'extra_header' => 'admin/partials/extra_headers/links.html',
- 'filters' => [
- 'status' => [
- 'title' => $lang->get('status'),
- 'options' => [
- '' => $lang->get('all'),
- '1' => $lang->get('active'),
- '0' => $lang->get('inactive'),
- ],
- ],
- 'order' => [
- 'title' => $lang->get('sort_by'),
- 'options' => [
- 'title' => $lang->get('title'),
- 'url' => $lang->get('url'),
- 'status' => $lang->get('status'),
- 'order' => $lang->get('order'),
- ],
- ],
- 'sort' => [
- 'options' => [
- 'asc' => $lang->get('ascending'),
- 'desc' => $lang->get('descending'),
- ],
- ],
- ],
- 'defaults' => [
- 'order' => $link_mod::DEFAULT_ORDER,
- 'sort' => $link_mod::DEFAULT_SORT,
- ],
+ $router->post('json:api/password-reset/request', function($body) use ($db, $lang, $user_mod, $view) {
+ $hash = bin2hex(random_bytes(18));
+ $user = $user_mod->get([
+ 'email' => $body['email'],
+ 'status' => 1,
]);
- });
- $router->get('admin/links/edit', function() use ($view, $link_mod) {
- $link = Helper::isValidId($_GET['id'] ?? false) ? $link_mod->get([ 'id' => $_GET['id'] ]) : [];
- if (!$link && isset($_GET['id'])) {
- http_response_code(404);
- return;
+ if ($user) {
+ $db->replace('password_restores', [
+ 'user_id' => $user['id'],
+ 'hash' => $hash,
+ 'created_at' => time(),
+ ]);
+ \Aurora\Core\Kernel::config('mail')($user['email'], $lang->get('restore_your_password'), $view->get('emails/password_restore.html', [ 'hash' => $hash ]));
}
- return $view->get('admin/link.html', [
- 'link' => $link,
- ]);
+ return json_encode([ 'success' => true ]);
});
- $router->post('json:admin/links/remove', function() use ($lang, $link_mod) {
- if (!\Aurora\App\Permission::can('edit_links')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
- }
+ $router->post('json:api/password-reset/confirm', function($body) use ($db, $user_mod, $login) {
+ $hash = $body['hash'] ?? '';
+ $password = $body['password'] ?? '';
+ $restore = $db->query('SELECT * FROM password_restores WHERE hash = ?', $hash)->fetch();
- if (!$link_mod->remove(explode(',', $_POST['id']))) {
- http_response_code(500);
- return json_encode([ 'errors' => [ $lang->get('unexpected_error') ] ]);
+ if (empty($restore) || $restore['created_at'] < strtotime('-2 hours')) {
+ return json_encode([
+ 'success' => false,
+ 'error' => 'expired_restore',
+ ]);
}
- return json_encode([ 'success' => true ]);
- });
+ $error = $user_mod->checkPassword($password, $body['password_confirm'] ?? '');
+ if (empty($error)) {
+ $user = $user_mod->get([
+ 'id' => $restore['user_id'],
+ 'status' => 1,
+ ]);
- /* TAGS */
-
- $router->get('admin/tags', function() use ($view, $lang, $tag_mod) {
- return $view->get('admin/list.html', [
- 'title' => $lang->get('tags'),
- 'show_add_button' => \Aurora\App\Permission::can('edit_tags'),
- 'columns' => [
- [ 'title' => '', 'class' => 'w100' ],
- [ 'title' => $lang->get('slug'), 'class' => 'w30' ],
- [ 'title' => $lang->get('number_posts'), 'class' => 'w10 numeric' ],
- [ 'title' => '', 'class' => 'w10 row-actions' ],
- ],
- 'extra_header' => 'admin/partials/extra_headers/tags.html',
- 'filters' => [
- 'order' => [
- 'title' => $lang->get('sort_by'),
- 'options' => [
- 'name' => $lang->get('name'),
- 'slug' => $lang->get('slug'),
- 'posts' => $lang->get('number_posts'),
- ],
- ],
- 'sort' => [
- 'options' => [
- 'asc' => $lang->get('ascending'),
- 'desc' => $lang->get('descending'),
- ],
- ],
- ],
- 'defaults' => [
- 'order' => $tag_mod::DEFAULT_ORDER,
- 'sort' => $tag_mod::DEFAULT_SORT,
- ],
- ]);
- });
+ if (!$user) {
+ return json_encode([
+ 'success' => false,
+ 'error' => 'no_active_user',
+ ]);
+ }
- $router->get('admin/tags/edit', function() use ($view, $tag_mod) {
- $tag = Helper::isValidId($_GET['id'] ?? false) ? $tag_mod->get([ 'id' => $_GET['id'] ]) : [];
- if (!$tag && isset($_GET['id'])) {
- http_response_code(404);
- return;
+ $db->delete('password_restores', $hash, 'hash');
+ $db->query('DELETE FROM tokens WHERE user_id = ?', $user['id']);
+ $db->update($user_mod->getTable(), [ 'password' => $user_mod->getPassword($password) ], $user['id']);
+ return json_encode($login($user['id']));
}
- return $view->get('admin/tag.html', [
- 'tag' => $tag,
+ return json_encode([
+ 'success' => false,
+ 'error' => $error,
]);
});
- $router->post('json:admin/tags/remove', function() use ($lang, $tag_mod) {
- if (!\Aurora\App\Permission::can('edit_tags')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ $router->middleware('api/*', function() use ($db, &$user) {
+ $path = Helper::getCurrentPath();
+
+ if (str_starts_with($path, 'api/blog')) {
+ return;
}
- if (!$tag_mod->remove(explode(',', $_POST['id']))) {
- http_response_code(500);
- return json_encode([ 'errors' => [ $lang->get('unexpected_error') ] ]);
+ if (empty($user) && !in_array($path, [ 'api/auth', 'api/password-reset/request', 'api/password-reset/confirm', 'api/logout' ])) {
+ return [ '', 401 ];
}
- return json_encode([ 'success' => true ]);
+ if (!empty($user['id'])) {
+ $now = time();
+ $db->query('UPDATE tokens
+ SET updated_at = ?, user_agent = ?
+ WHERE token = ?', $now, $_SERVER['HTTP_USER_AGENT'] ?? '', $user['token']);
+ $db->query('UPDATE users SET last_active = ? WHERE id = ?', $now, $user['id']);
+ }
});
- /* MEDIA */
+ $router->post('json:api/auth', function($body) use ($user_mod, $login) {
+ $user_id = $user_mod->authenticate((string) ($body['email'] ?? ''), (string) ($body['password'] ?? ''));
- $router->get('admin/media', function() use ($view, $lang) {
- $folders = [ Kernel::config('content') => '/' ];
- $root_dir = Helper::getPath();
- $content_dir = Helper::getPath(Kernel::config('content'));
- $path = $_GET['path'] ?? Kernel::config('content');
- $absolute_path = Helper::getPath($path);
+ return json_encode($user_id === false
+ ? [ 'success' => false, 'error' => 'invalid_credentials' ]
+ : $login($user_id));
+ });
- if ($path == Kernel::config('content') && !file_exists($absolute_path)) {
- mkdir($absolute_path, \Aurora\App\Media::FOLDER_PERMISSION);
- }
+ $router->post('json:api/logout', function() use ($db, $getAuthToken, $setAuthToken) {
+ $token = $getAuthToken();
- if (!\Aurora\App\Media::isValidPath($absolute_path) || !file_exists($absolute_path)) {
- http_response_code(404);
- return;
+ if ($token) {
+ $db->delete('tokens', $token, 'token');
}
- foreach (new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator($content_dir, FilesystemIterator::SKIP_DOTS),
- RecursiveIteratorIterator::SELF_FIRST
- ) as $file) {
- if ($file->isDir()) {
- $folder_dir = $file->getPathname();
- $folders[mb_substr($folder_dir, mb_strlen($root_dir) + 1)] = mb_substr($folder_dir, mb_strlen($content_dir) + 1);
+ return json_encode([ 'success' => $setAuthToken('', time() - 3600) ]);
+ });
+
+ $router->get('json:api/me', function() use (&$user) {
+ $me = $user;
+
+ if ($me) {
+ unset($me['password'], $me['token']);
+ foreach (\Aurora\App\Permission::getPermissions() as $action) {
+ $me['actions'][$action] = \Aurora\App\Permission::can($action);
}
}
- natcasesort($folders);
+ return json_encode($me);
+ });
- return $view->get('admin/list.html', [
- 'title' => $lang->get('media'),
- 'custom_header' => $view->get('admin/partials/media_header.html', [
- 'path' => $path,
- 'folders' => $folders,
- ]),
- 'columns' => [
- [ 'title' => '', 'class' => 'w100' ],
- [ 'title' => $lang->get('information'), 'class' => 'w20 file-info' ],
- [ 'title' => $lang->get('last_modification'), 'class' => 'w20' ],
- [ 'title' => '', 'class' => 'w10 row-actions' ],
- ],
- 'extra_header' => 'admin/partials/extra_headers/media.html',
- 'filters' => [
- 'order' => [
- 'title' => $lang->get('sort_by'),
- 'options' => [
- 'name' => $lang->get('name'),
- 'type' => $lang->get('type'),
- 'size' => $lang->get('size'),
- ]
- ],
- 'sort' => [
- 'options' => [
- 'asc' => $lang->get('ascending'),
- 'desc' => $lang->get('descending'),
- ],
- ],
- ],
- 'defaults' => [
- 'order' => 'type',
- 'sort' => 'asc',
- ],
- ]);
+ $router->get('json:api/me/sessions', function() use ($db, &$user) {
+ return json_encode(array_map(fn($t) => [
+ 'id' => $t['id'],
+ 'user_agent' => $t['user_agent'],
+ 'ip' => $t['ip'],
+ 'current' => $t['token'] == $user['token'],
+ 'created_at' => $t['created_at'],
+ 'updated_at' => $t['updated_at'],
+ ], $db->query('SELECT * FROM tokens WHERE user_id = ?', $user['id'])->fetchAll()));
});
- $router->post('json:admin/media/upload', function() use ($lang) {
- if (!\Aurora\App\Permission::can('edit_media')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
- }
+ $router->delete('json:api/me/sessions/{id}', function() use ($db, &$user) {
+ $stmt = $db->query('DELETE FROM tokens WHERE id = ? AND user_id = ?', $_GET['id'], $user['id']);
+ return json_encode([ 'success' => $stmt->rowCount() > 0 ]);
+ });
- $success = true;
- $path = $_GET['path'] ?? Kernel::config('content');
- $files = [];
+ $router->get('json:api/settings', function() use ($db, $lang) {
+ $settings = \Aurora\App\Setting::get();
- if (isset($_FILES['file']['name']) && !is_array($_FILES['file']['name'])) {
- $files[] = $_FILES['file'];
- } else {
- foreach (array_keys($_FILES['file']['name'] ?? []) as $i) {
- foreach (array_keys($_FILES['file']) as $prop) {
- $files[$i][$prop] = $_FILES['file'][$prop][$i];
- }
- }
+ if (!empty($settings['logo'])) {
+ $settings['logo'] = Helper::getContentPath($settings['logo']);
}
- foreach ($files as $file) {
- if (!\Aurora\App\Media::uploadFile($file, $path)) {
- $success = false;
- }
+ if (!\Aurora\App\Permission::can('edit_settings')) {
+ return json_encode(array_intersect_key($settings,
+ array_flip([ 'blog_url', 'views_count', 'language', 'timezone', 'date_format' ])));
}
+ $themes_dir = Helper::getPath(Kernel::config('views') . '/themes');
+
return json_encode([
- 'success' => $success,
- 'errors' => $success ? [] : [ $lang->get('error_upload_file') ],
+ ...$settings,
+ 'meta' => [
+ 'roles' => $db->query('SELECT * FROM roles ORDER BY level ASC')->fetchAll(),
+ 'themes' => array_filter(scandir($themes_dir), fn($file) => is_dir("$themes_dir/$file") && $file != '.' && $file != '..'),
+ 'languages' => $lang->getAll(),
+ 'timezones' => \DateTimeZone::listIdentifiers(),
+ 'db_dsn' => $db->dsn,
+ ],
]);
});
- $router->post('json:admin/media/create_folder', function() use ($lang) {
- if (!\Aurora\App\Permission::can('edit_media')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
- }
+ $router->post('json:api/users/impersonate', function($body) use ($user_mod, $login, &$user) {
+ $subject = $user_mod->get([
+ 'id' => $body['id'] ?? 0,
+ 'status' => 1,
+ ]);
- try {
- $success = \Aurora\App\Media::addFolder($_GET['path'] ?? Kernel::config('content'), $_POST['name'] ?? '');
- } catch (Exception) {
- $success = false;
+ if (!\Aurora\App\Permission::can('impersonate') || empty($subject) || (int) ($subject['role'] ?? 0) >= (int) ($user['role'] ?? 0)) {
+ return [ '', 403 ];
}
- return json_encode([
- 'success' => $success,
- 'errors' => $success ? [] : [ $lang->get('error_create_folder') ],
- ]);
+ return json_encode($login($subject['id']));
});
- $router->post('json:admin/media/remove', function() use ($lang) {
+ $router->post('json:api/media/create_folder', function($body) {
if (!\Aurora\App\Permission::can('edit_media')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ return [ '', 403 ];
}
- $paths = json_decode($_POST['paths'] ?? '') ?? [];
- $done = 0;
-
try {
- foreach ($paths as $path) {
- $done += \Aurora\App\Media::remove($path);
- }
-
- $success = $done == count($paths);
+ $success = \Aurora\App\Media::addFolder(Kernel::config('content') . '/' . ltrim($body['name'] ?? '', '/'));
} catch (Exception) {
$success = false;
}
- return json_encode([
- 'success' => $success,
- 'errors' => $success
- ? []
- : [ $lang->get($done == 0 ? 'error_remove_item' : 'error_remove_some_items') ],
- ]);
+ return json_encode([ 'success' => $success ]);
});
- $router->post('json:admin/media/save', function() use ($lang) {
- if (empty($_POST['name']) || str_contains($_POST['name'], '/')) {
+ $router->post('json:api/media/duplicate', function($body) {
+ if (empty($body['name']) || str_contains($body['name'], '/')) {
return json_encode([
'success' => false,
- 'errors' => [ 'name' => $lang->get('invalid_value') ]
+ 'error' => 'invalid_value',
]);
}
if (!\Aurora\App\Permission::can('edit_media')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ return [ '', 403 ];
}
try {
- $success = \Aurora\App\Media::rename($_POST['path'] ?? '', $_POST['name']);
+ $success = \Aurora\App\Media::duplicate($body['path'] ?? '', $body['name']);
} catch (Exception) {
$success = false;
}
- return json_encode([
- 'success' => $success,
- 'errors' => $success ? [] : [ $lang->get('error_rename_item') ],
- ]);
+ return json_encode([ 'success' => $success ]);
});
- $router->post('json:admin/media/move', function() use ($lang) {
- if (!\Aurora\App\Permission::can('edit_media')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ $router->post('json:api/media/rename', function($body) {
+ if (empty($body['name']) || str_contains($body['name'], '/')) {
+ return json_encode([ 'success' => false ]);
}
- $paths = json_decode($_POST['paths'] ?? '') ?? [];
- $done = 0;
+ if (!\Aurora\App\Permission::can('edit_media')) {
+ return [ '', 403 ];
+ }
try {
- foreach ($paths as $path) {
- $done += \Aurora\App\Media::move($path, $_POST['name']);
- }
-
- $success = $done == count($paths);
+ $success = \Aurora\App\Media::rename($body['path'] ?? '', $body['name']);
} catch (Exception) {
$success = false;
}
- return json_encode([
- 'success' => $success,
- 'errors' => $success
- ? []
- : [ $lang->get($done == 0 ? 'error_move_item' : 'error_move_some_items') ],
- ]);
+ return json_encode([ 'success' => $success ]);
});
- $router->post('json:admin/media/duplicate', function() use ($lang) {
- if (empty($_POST['name']) || str_contains($_POST['name'], '/')) {
- return json_encode([
- 'success' => false,
- 'errors' => [ 'name' => $lang->get('invalid_value') ]
- ]);
- }
-
+ $router->post('json:api/media/move', function($body) {
if (!\Aurora\App\Permission::can('edit_media')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ return [ '', 403 ];
}
try {
- $success = \Aurora\App\Media::duplicate($_POST['path'] ?? '', $_POST['name']);
+ $success = \Aurora\App\Media::move($body['path'] ?? '', $body['name']);
} catch (Exception) {
$success = false;
}
- return json_encode([
- 'success' => $success,
- 'errors' => $success ? [] : [ $lang->get('error_duplicate_item') ],
- ]);
+ return json_encode([ 'success' => $success ]);
});
- $router->get('admin/image_dialog', function() use ($view) {
- $path = $_GET['path'] ?? Kernel::config('content');
- $files = \Aurora\App\Media::getFiles($path, '', 'name', true, true);
- if ($files === false) {
- http_response_code(404);
- return;
+ $router->post('json:api/media/upload', function() {
+ if (!\Aurora\App\Permission::can('edit_media')) {
+ return [ '', 403 ];
}
- return $view->get('admin/partials/images_dialog.html', [
- 'path' => $path,
- 'files' => array_filter($files, fn($file) => !$file['is_file'] || $file['is_image']),
- ]);
- });
+ $success = true;
+ $path = Kernel::config('content') . '/' . ltrim($_GET['path'] ?? '', '/');
+ $files = [];
- /* SETTINGS */
+ if (isset($_FILES['file']['name']) && !is_array($_FILES['file']['name'])) {
+ $files[] = $_FILES['file'];
+ } else {
+ foreach (array_keys($_FILES['file']['name'] ?? []) as $i) {
+ foreach (array_keys($_FILES['file']) as $prop) {
+ $files[$i][$prop] = $_FILES['file'][$prop][$i];
+ }
+ }
+ }
- $router->get('admin/settings', function() use ($view, $db, $lang) {
- $themes_dir = Helper::getPath(Kernel::config('views') . '/themes');
+ foreach ($files as $file) {
+ if (!\Aurora\App\Media::uploadFile($file, $path)) {
+ $success = false;
+ }
+ }
- return $view->get('admin/settings.html', [
- 'roles' => $db->query('SELECT * FROM roles ORDER BY level ASC')->fetchAll(),
- 'themes' => array_filter(scandir($themes_dir), fn($file) => is_dir("$themes_dir/$file") && $file != '.' && $file != '..'),
- 'languages' => $lang->getAll(),
- 'timezones' => \DateTimeZone::listIdentifiers(),
- 'db_dsn' => $db->dsn,
- ]);
+ return json_encode([ 'success' => $success ]);
});
- $router->post('json:admin/settings/save', function() use ($db, $lang) {
- if (!\Aurora\App\Permission::can('edit_settings')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ $router->get('api/media/download', function() {
+ $file_path = Helper::getPath('content.zip');
+ $path = Helper::getPath(Kernel::config('content') . '/' . ltrim($_GET['path'] ?? '', '/'));
+
+ if (!\Aurora\App\Media::isValidPath($path)) {
+ return [ '', 403 ];
}
- try {
- $db->connection->beginTransaction();
+ $zip = new ZipArchive();
+ $zip->open($file_path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
- foreach ($_POST as $key => $val) {
- $db->replace('settings', [ 'key' => $key, 'value' => $val ]);
+ foreach (\Aurora\Core\Helper::getFileIterator($path) as $file) {
+ $real_path = $file->getRealPath();
+ $relative_path = mb_substr($real_path, mb_strlen($path) + 1);
+
+ if (!$file->isDir()) {
+ $zip->addFile($real_path, $relative_path);
+ } elseif ($relative_path !== false) {
+ $zip->addEmptyDir($relative_path);
}
+ }
- $success = $db->connection->commit();
- } catch (\PDOException $e) {
- $db->connection->rollBack();
- error_log($e->getMessage());
- $success = false;
+ $zip->close();
+ Helper::downloadFile($file_path, 'media.zip', 'application/zip');
+ });
+
+ $router->get('json:api/media/folders', function() {
+ $folders = [ '/' ];
+ $content_dir = Helper::getPath(Kernel::config('content'));
+
+ foreach (new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($content_dir, FilesystemIterator::SKIP_DOTS),
+ RecursiveIteratorIterator::SELF_FIRST
+ ) as $file) {
+ if ($file->isDir()) {
+ $folders[] = mb_substr($file->getPathname(), mb_strlen($content_dir) + 1);
+ }
}
- return json_encode([ 'success' => $success ]);
+ natcasesort($folders);
+
+ return json_encode(array_values($folders));
});
- $router->get('json:admin/{mod}/page', function() use ($kernel, $view, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) {
- $mod_str = $_GET['mod'] ?? '';
- switch ($mod_str) {
- case 'pages': $mod = $page_mod; break;
- case 'posts': $mod = $post_mod; break;
- case 'users': $mod = $user_mod; break;
- case 'tags': $mod = $tag_mod; break;
- case 'links': $mod = $link_mod; break;
- case 'media':
- $files = \Aurora\App\Media::getFiles($_GET['path'] ?? Kernel::config('content'), $_GET['search'] ?? '', $_GET['order'] ?? 'type', ($_GET['sort'] ?? 'asc') == 'asc');
- return json_encode([
- 'next_page' => false,
- 'count' => count($files),
- 'html' => $view->get('admin/partials/lists/media.html', [ 'files' => $files ]),
- ]);
- default:
- http_response_code(404);
- return;
+ $router->any('json:api/media/upload_image', function() {
+ if (!\Aurora\App\Permission::can('edit_media')) {
+ return [ '', 403 ];
}
- $per_page = $kernel->config('per_page');
- $where = $mod->getCondition($_GET);
+ $file_path = \Aurora\App\Media::uploadFile($_FILES['file'], Kernel::config('content') . '/' . date('Y/m/'));
- return json_encode([
- 'next_page' => $mod->isNextPageAvailable($_GET['page'], $per_page, $where),
- 'count' => $mod->count($where),
- 'html' => $view->get("admin/partials/lists/$mod_str.html", [
- $mod_str => $mod->getPage($_GET['page'], $per_page, $where, $_GET['order'] ?? $mod::DEFAULT_ORDER, ($_GET['sort'] ?? ($mod::DEFAULT_SORT ?? 'asc')) == 'asc'),
- ]),
- ]);
+ if ($file_path === false) {
+ return [ '', 500 ];
+ }
+
+ return json_encode([ 'location' => '/' . $file_path ]);
});
- $router->post('json:admin/{mod}/save', function() use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) {
- switch ($_GET['mod']) {
- case 'pages': $mod = $page_mod; break;
- case 'posts': $mod = $post_mod; break;
- case 'users': $mod = $user_mod; break;
- case 'tags': $mod = $tag_mod; break;
- case 'links': $mod = $link_mod; break;
- default:
- http_response_code(404);
- return;
+ $router->post('json:api/media', function() {
+ if (!\Aurora\App\Permission::can('edit_media')) {
+ return [ '', 403 ];
}
- $id = $_GET['id'] ?? '';
- $errors = $mod->checkFields($_POST, $id);
- if (!empty($errors)) {
- return json_encode([
- 'success' => false,
- 'errors' => $errors,
- ]);
+ $success = true;
+ $path = Kernel::config('content') . '/' . ltrim($_GET['path'] ?? '', '/');
+ $files = [];
+
+ if (isset($_FILES['file']['name']) && !is_array($_FILES['file']['name'])) {
+ $files[] = $_FILES['file'];
+ } else {
+ foreach (array_keys($_FILES['file']['name'] ?? []) as $i) {
+ foreach (array_keys($_FILES['file']) as $prop) {
+ $files[$i][$prop] = $_FILES['file'][$prop][$i];
+ }
+ }
}
- $success = Helper::isValidId($id)
- ? $mod->save($id, $_POST)
- : ($id = $mod->add($_POST)) !== false;
+ foreach ($files as $file) {
+ if (!\Aurora\App\Media::uploadFile($file, $path)) {
+ $success = false;
+ }
+ }
- return json_encode([
- 'success' => $success,
- 'id' => $id,
- ]);
+ return json_encode([ 'success' => $success ]);
});
- $router->get('json:admin/settings/db', function() use ($db) {
+ $router->get('json:api/db', function() use ($db) {
if (!\Aurora\App\Permission::can('edit_settings')) {
- http_response_code(403);
- return;
+ return [ '', 403 ];
}
- header('Content-disposition: attachment; filename=db.json');
-
return json_encode([
'meta' => [
'created' => date('Y-m-d H:i:s'),
@@ -888,269 +622,317 @@
]);
});
- $router->post('json:admin/settings/db_upload', function() use ($db, $lang) {
+ $router->post('json:api/db', function() use ($db, $lang) {
if (!\Aurora\App\Permission::can('edit_settings')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ return [ '', 403 ];
}
- $errors = [];
+ $error = false;
try {
- $json = json_decode(file_get_contents($_FILES['db']['tmp_name'] ?? ''), true);
+ $json = json_decode(file_get_contents($_FILES['file']['tmp_name'] ?? ''), true);
$version = is_scalar($json['meta']['version'] ?? null) ? explode('.', (string) $json['meta']['version']) : null;
if (!isset($version) || explode('.', \Aurora\Core\Kernel::VERSION)[0] != $version[0]) {
- $errors[] = $lang->get('invalid_db_version');
+ $error = 'invalid_db_version';
} elseif (!(new \Aurora\App\Migration($db))->import($json['tables'] ?? false)) {
- $errors[] = $lang->get('invalid_db_file');
+ $error = 'invalid_db_file';
}
} catch (\Throwable) {
- $errors[] = $lang->get('invalid_db_file');
+ $error = 'invalid_db_file';
}
- return json_encode([
- 'success' => empty($errors),
- 'errors' => $errors,
- ]);
+ $data = [
+ 'success' => $error === false,
+ ];
+
+ if ($error !== false) {
+ $data['error'] = $error;
+ }
+
+ return json_encode($data);
});
- $router->post('json:admin/settings/reset_views_count', function() use ($db, $lang) {
+ $router->get('api/logs', function() {
if (!\Aurora\App\Permission::can('edit_settings')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ return [ '', 403 ];
}
- return json_encode([ 'success' => $db->delete('views') ]);
+ $path = \Aurora\Core\Helper::getPath(\Aurora\App\Setting::get('log_file'));
+ return file_exists($path) ? file_get_contents($path) : '';
});
- $router->post('json:admin/settings/logs_clear', function() use ($lang) {
+ $router->delete('json:api/logs', function() {
if (!\Aurora\App\Permission::can('edit_settings')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ return [ '', 403 ];
}
- return json_encode([ 'success' => unlink(Helper::getPath(\Aurora\App\Setting::get('log_file'))) ]);
- });
+ $path = Helper::getPath(\Aurora\App\Setting::get('log_file'));
- $router->get('admin/settings/logs_download', function() {
- Helper::downloadFile(Helper::getPath(\Aurora\App\Setting::get('log_file')), 'Aurora ' . date('Y-m-d H:i:s') . '.log', 'text/plain');
+ return json_encode([ 'success' => !file_exists($path) || (is_writable($path) && unlink($path)) ]);
});
- $router->get('admin/settings/media_download', function() use ($lang) {
- $file_path = Helper::getPath('content.zip');
- $path = $_GET['path'] ?? '';
- $absolute_path = Helper::getPath($path);
+ $router->post('json:api/reset_views_count', function() use ($db) {
+ if (!\Aurora\App\Permission::can('edit_settings')) {
+ return [ '', 403 ];
+ }
- if (!\Aurora\App\Media::isValidPath($absolute_path)) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ return json_encode([ 'success' => $db->delete('views') ]);
+ });
+
+ $router->post('json:api/settings', function($body) use ($db) {
+ if (!\Aurora\App\Permission::can('edit_settings')) {
+ return [ '', 403 ];
}
- $zip = new ZipArchive();
- $zip->open($file_path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+ try {
+ $db->connection->beginTransaction();
- foreach (\Aurora\Core\Helper::getFileIterator($absolute_path) as $file) {
- $real_path = $file->getRealPath();
- $relative_path = mb_substr($real_path, mb_strlen($absolute_path) + 1);
+ foreach ($body as $key => $val) {
+ if ($key === 'logo') {
+ $val = Helper::normalizeContentPath($val);
+ }
- if (!$file->isDir()) {
- $zip->addFile($real_path, $relative_path);
- } elseif ($relative_path !== false) {
- $zip->addEmptyDir($relative_path);
+ $db->replace('settings', [ 'key' => $key, 'value' => $val ]);
}
+
+ $success = $db->connection->commit();
+ } catch (\PDOException $e) {
+ $db->connection->rollBack();
+ error_log($e->getMessage());
+ $success = false;
}
- $zip->close();
- Helper::downloadFile($file_path, urldecode(trim($path, '/')) . ' ' . date('Y-m-d H:i:s') . '.zip', 'application/zip');
+ return json_encode([ 'success' => $success ]);
+ });
+
+ $router->get('json:api/server', function() use ($db) {
+ if (!\Aurora\App\Permission::can('edit_settings')) {
+ return [ '', 403 ];
+ }
+
+ return json_encode([
+ 'os' => php_uname('s') . ' ' . php_uname('r'),
+ 'php_version' => phpversion(),
+ 'db_dsn' => $db->dsn,
+ 'root_folder' => rtrim(\Aurora\Core\Helper::getPath(), '/'),
+ 'date' => date('Y-m-d H:i:s'),
+ 'memory_limit' => \Aurora\Core\Helper::getPhpSize(ini_get('memory_limit')),
+ 'file_size_limit' => \Aurora\App\Media::getMaxUploadFileSize(),
+ ]);
});
- $router->get('json:admin/settings/update_version', function() {
+ $router->get('json:api/update_version', function() {
+ if (!\Aurora\App\Permission::can('update')) {
+ return [ '', 403 ];
+ }
+
return json_encode((new \Aurora\App\Update())->getLatestRelease());
});
- $router->post('json:admin/settings/update', function() use ($lang) {
+ $router->post('json:api/update', function($body) {
if (!\Aurora\App\Permission::can('update')) {
- http_response_code(403);
- return json_encode([ 'errors' => [ $lang->get('no_permission') ] ]);
+ return [ '', 403 ];
}
- $result = (new \Aurora\App\Update)->run($_POST['zip'] ?? '');
+ $result = (new \Aurora\App\Update())->run($body['zip'] ?? '', fn($line) => Helper::log($line));
$error = match ($result) {
- \Aurora\App\Update::ERROR_CONNECTION => $lang->get('update_error_connection'),
- \Aurora\App\Update::ERROR_ZIP => $lang->get('update_error_zip'),
- \Aurora\App\Update::ERROR_COPY => $lang->get('update_error_copy'),
+ \Aurora\App\Update::ERROR_CONNECTION => 'update_error_connection',
+ \Aurora\App\Update::ERROR_ZIP => 'update_error_zip',
+ \Aurora\App\Update::ERROR_COPY => 'update_error_copy',
+ \Aurora\App\Update::ERROR_BUILD => 'update_error_build',
+ \Aurora\App\Update::ERROR_COMPOSER => 'update_error_composer',
default => null,
};
return json_encode([
'success' => $result === true,
- 'errors' => [ $error ],
+ 'error' => $error,
]);
});
- /**
- * BLOG
- */
+ $router->get('json:api/stats', function() use ($db, $post_mod) {
+ return json_encode([
+ 'total_posts' => $db->count('posts', '', $post_mod->getCondition([ 'status' => 1 ])),
+ 'total_scheduled_posts' => $db->count('posts', '', $post_mod->getCondition([ 'status' => 'scheduled' ])),
+ 'total_draft_posts' => $db->count('posts', '', 'status != 1'),
+ 'total_pages' => $db->count('pages', '', 'status = 1'),
+ 'total_draft_pages' => $db->count('pages', '', 'status != 1'),
+ 'total_users' => $db->count('users', '', 'status = 1'),
+ 'total_inactive_users' => $db->count('users', '', 'status != 1'),
+ ]);
+ });
- $router->get('json:api/posts', function() use ($view, $post_mod, $theme_dir) {
- $current_page = max(1, (int) ($_GET['page'] ?? 1));
- $per_page = \Aurora\App\Setting::get('per_page');
- $where = [ $post_mod->getCondition([ 'status' => 1 ]) ];
+ $router->get('json:api/view_files', function() use ($theme_dir) {
+ $absolute_theme_dir = Helper::getPath(Kernel::config('views') . "/$theme_dir");
+ $view_files = [];
- if (!empty($_GET['user'])) {
- $where[] = 'posts.user_id = ' . ((int) $_GET['user']);
+ foreach (Helper::getFileIterator($absolute_theme_dir) as $file) {
+ if ($file->isFile()) {
+ $view_files[] = mb_substr($file->getPathname(), mb_strlen($absolute_theme_dir) + 1);
+ }
}
- if (!empty($_GET['tag'])) {
- $where[] = 'posts.id IN (SELECT post_id FROM posts_to_tags WHERE tag_id = ' . ((int) $_GET['tag']) . ')';
+ natcasesort($view_files);
+ return json_encode(array_values($view_files));
+ });
+
+ $router->post('json:api/{mod}', function($body) use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod, &$user) {
+ switch ($_GET['mod']) {
+ case 'pages': $mod = $page_mod; break;
+ case 'posts': $mod = $post_mod; break;
+ case 'users': $mod = $user_mod; break;
+ case 'tags': $mod = $tag_mod; break;
+ case 'links': $mod = $link_mod; break;
+ default:
+ return [ '', 404 ];
}
- $where = implode(' AND ', $where);
+ $id = $_GET['id'] ?? '';
+ $errors = $mod->checkFields($body, $id, $user);
+
+ if (!empty($errors)) {
+ return [
+ json_encode([
+ 'success' => false,
+ 'errors' => $errors,
+ ]),
+ array_intersect([ 'no_permission', 'no_publish_permission' ], $errors) ? 403 : 200,
+ ];
+ }
return json_encode([
- 'next_page' => $post_mod->isNextPageAvailable($current_page, $per_page, $where),
- 'html' => $view->get("$theme_dir/partials/posts_page.html", [
- 'posts' => $post_mod->getPage($current_page, $per_page, $where),
- ]),
+ 'success' => Helper::isValidId($id)
+ ? $mod->save($id, $body, $user)
+ : ($id = $mod->add($body)) !== false,
+ 'id' => $id,
]);
});
- $router->get($blog_url, function() use ($db, $view, $lang, $link_mod, $post_mod, $theme_dir) {
- $current_page = max(1, (int) ($_GET['page'] ?? 1));
- $per_page = \Aurora\App\Setting::get('per_page');
- $where = $post_mod->getCondition([ 'status' => 1 ]);
- $search = $db->escape($_GET['search'] ?? '');
+ $router->delete('json:api/{mod}', function($body) use ($page_mod, $post_mod, $user_mod, $tag_mod, $link_mod, &$user) {
+ $ids = isset($body['id'])
+ ? array_map(fn($id) => (int) $id, is_array($body['id']) ? $body['id'] : explode(',', $body['id']))
+ : null;
+ $mod_str = $_GET['mod'] ?? '';
- if (!empty($search)) {
- $where .= " AND (posts.title LIKE '%$search%' OR posts.description LIKE '%$search%')";
+ if (!\Aurora\App\Permission::can("edit_$mod_str")) {
+ return [ '', 403 ];
}
- return $view->get("$theme_dir/blog.html", [
- 'header_links' => $link_mod->getHeaderLinks(),
- 'title' => $lang->get('blog'),
- 'posts' => $post_mod->getPage($current_page, $per_page, $where, 'date', false, true),
- 'next_page' => $post_mod->isNextPageAvailable($current_page, $per_page, $where),
- 'current_page' => $current_page,
- ]);
- });
+ if ($mod_str !== 'media' && empty($ids)) {
+ return [ '', 400 ];
+ }
- $router->get("$blog_url/author/{author}", function() use ($view, $user_mod, $link_mod, $post_mod, $theme_dir) {
- $current_page = max(1, (int) ($_GET['page'] ?? 1));
- $user = $user_mod->get([ 'slug' => $_GET['author'] ]);
- $per_page = \Aurora\App\Setting::get('per_page');
+ $success = match ($mod_str) {
+ 'pages' => $page_mod->remove($ids),
+ 'posts' => $post_mod->remove($ids),
+ 'tags' => $tag_mod->remove($ids),
+ 'links' => $link_mod->remove($ids),
+ 'users' => (function() use ($user_mod, $ids, &$user) {
+ $valid_ids = [];
- if (!$user) {
- http_response_code(404);
- return;
- }
+ foreach ($user_mod->getPage(null, null, 'users.id IN (' . implode(',', $ids) . ')') as $row) {
+ if (\Aurora\App\Permission::editUser($row) && $row['id'] != $user['id']) {
+ $valid_ids[] = $row['id'];
+ }
+ }
- $where = implode(' AND ', [
- $post_mod->getCondition([ 'status' => 1 ]),
- 'users.id = ' . ((int) $user['id']),
- ]);
+ $ids = $valid_ids;
+ return $user_mod->remove($ids);
+ })(),
+ 'media' => (function() use ($body) {
+ $done = 0;
- return $view->get("$theme_dir/blog.html", [
- 'header_links' => $link_mod->getHeaderLinks(),
- 'title' => $user['name'],
- 'user' => $user,
- 'posts' => $post_mod->getPage($current_page, $per_page, $where, 'date', false, true),
- 'next_page' => $post_mod->isNextPageAvailable($current_page, $per_page, $where),
- 'current_page' => $current_page,
- ]);
- });
+ try {
+ foreach ($body as $path) {
+ $done += \Aurora\App\Media::remove($path);
+ }
- $router->get("$blog_url/tag/{tag}", function() use ($view, $tag_mod, $link_mod, $post_mod, $theme_dir) {
- $current_page = max(1, (int) ($_GET['page'] ?? 1));
- $per_page = \Aurora\App\Setting::get('per_page');
- $tag = $tag_mod->get([ 'slug' => $_GET['tag'] ]);
+ $success = $done == count($body);
+ } catch (Exception) {
+ $success = false;
+ }
- if (!$tag) {
- http_response_code(404);
- return;
- }
+ return $success;
+ })(),
+ default => null,
+ };
- $where = implode(' AND ', [
- $post_mod->getCondition([ 'status' => 1 ]),
- 'posts.id IN (SELECT post_id FROM posts_to_tags WHERE tag_id = ' . ((int) $tag['id']) . ')',
- ]);
+ if ($success === null) {
+ return [ '', 404 ];
+ }
- return $view->get("$theme_dir/blog.html", [
- 'header_links' => $link_mod->getHeaderLinks(),
- 'title' => $tag['name'],
- 'tag' => $tag,
- 'posts' => $post_mod->getPage($current_page, $per_page, $where, 'date', false, true),
- 'next_page' => $post_mod->isNextPageAvailable($current_page, $per_page, $where),
- 'current_page' => $current_page,
- ]);
+ return json_encode([ 'success' => $success ]);
});
- $router->get("$blog_url/{slug}", function() use ($db, $view, $link_mod, $post_mod, $theme_dir) {
- $post_cond = $post_mod->getCondition([ 'status' => 1 ]);
+ $router->get('json:api/roles', function() use ($db) {
+ $roles = [];
+ $permissions_data = $db->query('SELECT role_level, permission FROM roles_permissions ORDER BY role_level ASC, permission ASC')->fetchAll();
+ foreach ($db->query('SELECT * FROM roles ORDER BY level ASC')->fetchAll() as $role) {
+ $role_permissions = [];
- $post = $post_mod->get([
- 'slug' => $_GET['slug'] ?? '',
- empty($_SESSION['user']) ? $post_cond : '',
- ]);
+ foreach ($permissions_data as $permission) {
+ if ($role['level'] >= $permission['role_level']) {
+ $role_permissions[] = $permission['permission'];
+ }
+ }
- if (!$post) {
- http_response_code(404);
- return;
- }
+ sort($role_permissions);
- if (\Aurora\App\Setting::get('views_count')) {
- $db->replace('views', [
- 'type' => 'post',
- 'item_id' => $post['id'],
- 'ip' => Helper::getUserIP(),
- 'date' => time(),
- ]);
+ $roles[] = [
+ 'level' => (int) $role['level'],
+ 'slug' => $role['slug'],
+ 'permissions' => $role_permissions
+ ];
}
- return $view->get("$theme_dir/post.html", [
- 'header_links' => $link_mod->getHeaderLinks(),
- 'post' => $post,
- 'related_posts' => empty($post['tags_id'])
- ? []
- : $post_mod->getPage(1, 3, "$post_cond AND p2t.tag_id IN (" . $post['tags_id'] . ') AND posts.id != ' . $post['id']),
- ]);
+ return json_encode($roles);
});
- if (!empty($rss)) {
- $router->get("xml:$rss", function() use ($post_mod, $view, $theme_dir) {
- return $view->get("$theme_dir/rss.html", [
- 'posts' => $post_mod->getPage(null, null, $post_mod->getCondition([ 'status' => 1 ]), 'date', false),
- ]);
- });
- }
-
- $router->get([ '/', '{slug}' ], function() use ($db, $view, $link_mod, $page_mod, $theme_dir) {
- $page = $page_mod->get([
- 'slug' => $_GET['slug'] ?? '',
- empty($_SESSION['user']) ? $page_mod->getCondition([ 'status' => 1 ]) : '',
- ]);
+ $router->get('json:api/{mod}', function() use ($kernel, $page_mod, $post_mod, $user_mod, $tag_mod, $link_mod) {
+ switch ($_GET['mod'] ?? '') {
+ case 'pages': $mod = $page_mod; break;
+ case 'posts': $mod = $post_mod; break;
+ case 'users': $mod = $user_mod; break;
+ case 'tags': $mod = $tag_mod; break;
+ case 'links': $mod = $link_mod; break;
+ case 'media':
+ $files = \Aurora\App\Media::getFiles(Kernel::config('content') . '/' . ltrim($_GET['path'] ?? '', '/'),
+ $_GET['search'] ?? '',
+ $_GET['order'] ?? 'type',
+ ($_GET['sort'] ?? 'asc') == 'asc');
- if (!$page) {
- http_response_code(404);
- return;
- }
+ if ($_GET['images'] ?? false) {
+ $files = array_filter($files, fn($file) => !$file['is_file'] || $file['is_image']);
+ }
- if (\Aurora\App\Setting::get('views_count')) {
- $db->replace('views', [
- 'type' => 'page',
- 'item_id' => $page['id'],
- 'ip' => Helper::getUserIP(),
- 'date' => time(),
- ]);
+ return json_encode([
+ 'data' => $files,
+ 'meta' => [
+ 'current_page' => 1,
+ 'per_page' => false,
+ 'prev_page' => false,
+ 'next_page' => false,
+ 'total_items' => count($files),
+ ],
+ ]);
+ default:
+ return [ '', 404 ];
}
- $template = !empty($page['static']) && !empty($page['static_file'])
- ? $page['static_file']
- : 'page.html';
+ $page = (int) max($_GET['page'] ?? 1, 1);
+ $per_page = $kernel->config('per_page');
+ $where = $mod->getCondition($_GET);
- return $view->get("$theme_dir/$template", [
- 'header_links' => $link_mod->getHeaderLinks(),
- ...$page,
+ return json_encode([
+ 'data' => $mod->getPage($page, $per_page, $where, $_GET['order'] ?? $mod::DEFAULT_ORDER, ($_GET['sort'] ?? ($mod::DEFAULT_SORT ?? 'asc')) == 'asc'),
+ 'meta' => [
+ 'current_page' => $page,
+ 'per_page' => $per_page,
+ 'prev_page' => $page > 1,
+ 'next_page' => $mod->isNextPageAvailable($page, $per_page, $where),
+ 'total_items' => $mod->count($where),
+ ],
]);
});
};
diff --git a/app/controllers/Media.php b/app/controllers/Media.php
index d569cbd..d972628 100644
--- a/app/controllers/Media.php
+++ b/app/controllers/Media.php
@@ -7,14 +7,14 @@ final class Media
public const FOLDER_PERMISSION = 0755;
/**
- * The content directory relative to the project root directory
+ * The content directory relative to the app root directory
* @var string
*/
private static string $directory = '';
/**
* Sets the content directory
- * @param string $directory the content directory relative to the project root directory
+ * @param string $directory the content directory relative to the app root directory
*/
public static function setDirectory(string $directory): void
{
@@ -24,7 +24,7 @@ public static function setDirectory(string $directory): void
/**
* Returns the files and folders in the given path
* @throws \InvalidArgumentException
- * @param string $path the path relative to the project root directory
+ * @param string $path the path relative to the app root directory
* @param [string] $search the search string
* @param [string] $order the order (name, type, size)
* @param [bool] $asc the order direction. true for ascending, false for descending
@@ -43,9 +43,11 @@ public static function getFiles(string $path, string $search = '', string $order
$files = array_map(function($file) use ($content_path_length) {
$mime = mime_content_type($file);
+ $relative_path = mb_substr($file, $content_path_length);
+
return [
'name' => basename($file),
- 'path' => mb_substr($file, $content_path_length),
+ 'path' => \Aurora\Core\Helper::getContentPath($relative_path),
'mime' => $mime,
'is_file' => is_file($file),
'is_image' => str_starts_with($mime, 'image/'),
@@ -80,29 +82,24 @@ public static function getFiles(string $path, string $search = '', string $order
/**
* Creates a new folder with the given name in the given path
* @throws \InvalidArgumentException
- * @param string $path the path relative to the project root directory
- * @param string $name the folder name
+ * @param string $path the folder path relative to the project root directory
* @return bool true if the folder was created successfully, false otherwise
*/
- public static function addFolder(string $path, string $name): bool
+ public static function addFolder(string $path): bool
{
$path = \Aurora\Core\Helper::getPath($path);
- if (empty(trim($name))) {
- throw new \InvalidArgumentException('Folder name is empty');
- }
-
if (!self::isValidPath($path)) {
throw new \InvalidArgumentException("Path '$path' is not a valid path within " . self::$directory);
}
- return mkdir("$path/$name", self::FOLDER_PERMISSION);
+ return mkdir($path, self::FOLDER_PERMISSION);
}
/**
* Deletes the file/folder with given path
* @throws \InvalidArgumentException
- * @param string $path the path relative to the project root directory
+ * @param string $path the path relative to the app root directory
* @return bool true if the file/folder was deleted successfully, false otherwise
*/
public static function remove(string $path): bool
@@ -119,7 +116,7 @@ public static function remove(string $path): bool
/**
* Renames the file/folder with given path
* @throws \InvalidArgumentException
- * @param string $path the path relative to the project root directory
+ * @param string $path the path relative to the app root directory
* @param string $name the new name
* @return bool true if the file/folder was renamed successfully, false otherwise
*/
@@ -137,8 +134,8 @@ public static function rename(string $path, string $name): bool
/**
* Moves the file/folder with given path to the given folder
* @throws \InvalidArgumentException
- * @param string $path the path relative to the project root directory
- * @param string $name the destination folder relative to the project root directory
+ * @param string $path the path relative to the app root directory
+ * @param string $folder the destination folder relative to the app root directory
* @return bool true if the file/folder was moved successfully, false otherwise
*/
public static function move(string $path, string $folder): bool
@@ -160,7 +157,7 @@ public static function move(string $path, string $folder): bool
/**
* Duplicates the file with given path with the given name
* @throws \InvalidArgumentException
- * @param string $path the path relative to the project root directory
+ * @param string $path the path relative to the app root directory
* @param string $name the new name
* @return bool true if the file was duplicated successfully, false otherwise
*/
@@ -200,13 +197,12 @@ public static function isValidPath(string $path): bool
* Uploads the given file to the given path
* @throws \InvalidArgumentException
* @param array $file the file
- * @param string $path the path relative to the project root directory
- * @return bool true if the file was uploaded successfully, false otherwise
+ * @param string $path the path relative to the app root directory
+ * @return string|false the file path relative to the app root directory on success, false otherwise
*/
- public static function uploadFile($file, string $path): bool
+ public static function uploadFile($file, string $path): string|false
{
$path = \Aurora\Core\Helper::getPath($path);
- $container_path = mb_substr($path, 0, mb_strrpos($path, '/') + 1);
if (!$file) {
throw new \InvalidArgumentException('File is empty');
@@ -216,11 +212,16 @@ public static function uploadFile($file, string $path): bool
throw new \InvalidArgumentException("Path '$path' is not a valid path within " . self::$directory);
}
+ $destination = self::getFilePath($path, $file['name']);
+ $container_path = dirname($destination);
+
if (!file_exists($container_path)) {
mkdir($container_path, self::FOLDER_PERMISSION, true);
}
- return move_uploaded_file($file['tmp_name'], self::getFilePath($path, $file['name']));
+ return move_uploaded_file($file['tmp_name'], $destination)
+ ? mb_substr($destination, mb_strlen(\Aurora\Core\Helper::getPath()) + 1)
+ : false;
}
/**
diff --git a/app/controllers/Migration.php b/app/controllers/Migration.php
index 0053e9d..d490806 100644
--- a/app/controllers/Migration.php
+++ b/app/controllers/Migration.php
@@ -71,6 +71,16 @@ final class Migration
'meta_title' => 'TEXT',
'meta_description' => 'TEXT',
],
+ 'tokens' => [
+ 'id' => 'INTEGER PRIMARY KEY',
+ 'user_id' => 'INTEGER',
+ 'token' => 'TEXT',
+ 'user_agent' => 'TEXT',
+ 'ip' => 'TEXT',
+ 'created_at' => 'INTEGER',
+ 'updated_at' => 'INTEGER',
+ '' => 'CONSTRAINT tokens_pk UNIQUE (`user_id`, `token`)',
+ ],
'users' => [
'id' => 'INTEGER PRIMARY KEY',
'name' => 'TEXT',
diff --git a/app/controllers/ModuleBase.php b/app/controllers/ModuleBase.php
index e9ddb15..5218c2d 100644
--- a/app/controllers/ModuleBase.php
+++ b/app/controllers/ModuleBase.php
@@ -169,6 +169,15 @@ public function remove(array $ids): bool
return $success;
}
+ /**
+ * Returns the table name
+ * @return string the table name
+ */
+ public function getTable(): string
+ {
+ return $this->table;
+ }
+
/**
* Returns the row with additional data mapped into it
* @param mixed $data the row
diff --git a/app/controllers/ModuleInterface.php b/app/controllers/ModuleInterface.php
index b29fc69..e6c37b3 100644
--- a/app/controllers/ModuleInterface.php
+++ b/app/controllers/ModuleInterface.php
@@ -6,9 +6,9 @@ interface ModuleInterface
{
public function add(array $data): string|bool;
- public function save(int $id, array $data): bool;
+ public function save(int $id, array $data, ?array $user = null): bool;
- public function checkFields(array $data, $id = null): array;
+ public function checkFields(array $data, $id, $user): array;
public function getCondition(array $search): string;
}
diff --git a/app/controllers/Permission.php b/app/controllers/Permission.php
index 6a1984c..0409214 100644
--- a/app/controllers/Permission.php
+++ b/app/controllers/Permission.php
@@ -66,4 +66,13 @@ public static function can(string $key): bool
return self::$user_role >= self::$permissions[$key];
}
+
+ /**
+ * Returns the list of permission keys
+ * @return array the list of permission keys
+ */
+ public static function getPermissions(): array
+ {
+ return array_keys(self::$permissions);
+ }
}
diff --git a/app/controllers/Update.php b/app/controllers/Update.php
index 217496b..ee523e7 100644
--- a/app/controllers/Update.php
+++ b/app/controllers/Update.php
@@ -5,16 +5,16 @@
final class Update
{
private const UPDATE_DIRECTORIES = [
- 'app/bootstrap/index.php',
- 'app/bootstrap/routes.php',
+ 'app/bootstrap',
'app/controllers',
+ 'app/core',
'app/database',
'app/languages',
+ 'app/public',
+ 'app/react',
'app/views',
'bin',
- 'core',
'docs',
- 'public',
'tests',
'.htaccess',
'aurora',
@@ -23,42 +23,139 @@ final class Update
public const ERROR_CONNECTION = 1;
public const ERROR_ZIP = 2;
public const ERROR_COPY = 3;
+ public const ERROR_BUILD = 4;
+ public const ERROR_COMPOSER = 5;
/**
* Updates the system to the given release zip
- * @param string path to the release zip file
+ * @param string $zip path to the release zip file
+ * @param callable|null $on_build_output optional callback invoked with each line of composer and React build output
* @return int|bool true on success, an error code otherwise
*/
- public function run(string $zip): int|bool
+ public function run(string $zip, ?callable $on_build_output = null): int|bool
{
- $temp_dir = sys_get_temp_dir();
- $zip_dir = tempnam($temp_dir, 'aurora-update');
+ $temp = sys_get_temp_dir();
+ $zip_file = tempnam($temp, 'aurora-update');
- if (!file_put_contents($zip_dir, fopen($zip, 'r', false, self::getStreamContext()))) {
+ if (!file_put_contents($zip_file, fopen($zip, 'r', false, self::getStreamContext()))) {
return self::ERROR_CONNECTION;
}
- $zip = new \ZipArchive();
-
- if ($zip->open($zip_dir) !== true || !$zip->extractTo($temp_dir) || !($index = $zip->getNameIndex(0))) {
+ $archive = new \ZipArchive();
+ if ($archive->open($zip_file) !== true || !$archive->extractTo($temp) || !($index = $archive->getNameIndex(0))) {
+ @unlink($zip_file);
return self::ERROR_ZIP;
}
- $new_version_dir = "$temp_dir/" . trim($index, '/');
+ $archive->close();
+ @unlink($zip_file);
- if (!$zip->close()) {
- return self::ERROR_ZIP;
+ $root = \Aurora\Core\Helper::getProjectPath();
+ $update = "$temp/" . trim($index, '/');
+ $backup = "$temp/" . uniqid('aurora-backup-');
+ mkdir($backup);
+
+ foreach (self::UPDATE_DIRECTORIES as $dir) {
+ if (file_exists("$root/$dir") && !\Aurora\Core\Helper::copy("$root/$dir", "$backup/$dir")) {
+ \Aurora\Core\Helper::removeDirRecursive($backup);
+ return self::ERROR_COPY;
+ }
}
foreach (self::UPDATE_DIRECTORIES as $dir) {
- if (!\Aurora\Core\Helper::copy("$new_version_dir/$dir", \Aurora\Core\Helper::getPath("/$dir"))) {
+ if (!file_exists("$update/$dir")) {
+ continue;
+ }
+
+ if (!\Aurora\Core\Helper::copy("$update/$dir", "$root/$dir")) {
+ $this->restore($backup, $root);
+ \Aurora\Core\Helper::removeDirRecursive($backup);
return self::ERROR_COPY;
}
}
+ if (!$this->installComposer($on_build_output)) {
+ $this->restore($backup, $root);
+ \Aurora\Core\Helper::removeDirRecursive($backup);
+ return self::ERROR_COMPOSER;
+ }
+
+ if (!$this->buildReact($on_build_output)) {
+ $this->restore($backup, $root);
+ \Aurora\Core\Helper::removeDirRecursive($backup);
+ return self::ERROR_BUILD;
+ }
+
+ \Aurora\Core\Helper::removeDirRecursive($backup);
return true;
}
+ /**
+ * Installs PHP dependencies via Composer
+ * @param callable|null $on_output optional callback invoked with each line of Composer output
+ * @return bool true if dependencies were installed successfully, false otherwise
+ */
+ private function installComposer(?callable $on_output = null): bool
+ {
+ $root = \Aurora\Core\Helper::getProjectPath();
+
+ if (!is_file("$root/composer.json")) {
+ return true;
+ }
+
+ $output = [];
+ $return_var = 0;
+ exec('composer install --no-interaction --prefer-dist --working-dir "' . $root . '" 2>&1', $output, $return_var);
+
+ if ($on_output && $output) {
+ foreach ($output as $line) {
+ $on_output($line);
+ }
+ }
+
+ return $return_var === 0;
+ }
+
+ /**
+ * Builds the React admin panel
+ * @param callable|null $on_output optional callback invoked with each line of React build output
+ * @return bool true if the React admin panel was built successfully, false otherwise
+ */
+ private function buildReact(?callable $on_output = null): bool
+ {
+ $react_dir = \Aurora\Core\Helper::getPath('react');
+
+ if (!is_file("$react_dir/package.json")) {
+ return true;
+ }
+
+ $output = [];
+ $return_var = 0;
+ exec('npm --prefix "' . $react_dir . '" install && npm --prefix "' . $react_dir . '" run build 2>&1', $output, $return_var);
+
+ if ($on_output && $output) {
+ foreach ($output as $line) {
+ $on_output($line);
+ }
+ }
+
+ return $return_var === 0;
+ }
+
+ /**
+ * Restores the backup files
+ * @param string $backup the backup directory
+ * @param string $root the root directory
+ */
+ private function restore(string $backup, string $root): void
+ {
+ foreach (self::UPDATE_DIRECTORIES as $dir) {
+ if (file_exists("$backup/$dir")) {
+ \Aurora\Core\Helper::copy("$backup/$dir", "$root/$dir");
+ }
+ }
+ }
+
/**
* Returns an array with data about the latest release compatible with the current version (same major version)
* @return array|bool|int the array with data about the latest release,
diff --git a/app/controllers/ViewHelper.php b/app/controllers/ViewHelper.php
index 8dfbf42..2dd0651 100644
--- a/app/controllers/ViewHelper.php
+++ b/app/controllers/ViewHelper.php
@@ -88,32 +88,4 @@ public function getLanguageCode(): string
{
return $this->language->getCode();
}
-
- /**
- * @see \Aurora\Core\Helper::getUrl
- */
- public function url(string $path = ''): string
- {
- return \Aurora\Core\Helper::getUrl($path);
- }
-
- /**
- * Returns the current CSRF token, it creates it if it's not set
- * @return string the CSRF token
- */
- public function csrfToken(): string
- {
- if (!isset($_COOKIE['csrf_token'])) {
- $token = bin2hex(random_bytes(8));
-
- $_COOKIE['csrf_token'] = $token;
- setcookie('csrf_token', $token, [
- 'path' => '/',
- 'httponly' => true,
- 'samesite' => 'Lax',
- ]);
- }
-
- return $_COOKIE['csrf_token'];
- }
}
diff --git a/app/controllers/modules/Link.php b/app/controllers/modules/Link.php
index 53d12dd..51946b5 100755
--- a/app/controllers/modules/Link.php
+++ b/app/controllers/modules/Link.php
@@ -38,30 +38,31 @@ public function add(array $data): string|bool
* Updates an existing link
* @param int $id the link id
* @param array $data the new data
+ * @param array|null $user the user performing the action
* @return bool true on success, false otherwise
*/
- public function save(int $id, array $data): bool
+ public function save(int $id, array $data, ?array $user = null): bool
{
- return $this->db->update($this->table, $this->getBaseData($data), $id) ? $id : false;
+ return $this->db->update($this->table, $this->getBaseData($data), $id);
}
/**
* Returns an array with all the link fields that contain an error
* @param array $data the link fields
- * @param [mixed] $id the link id
+ * @param mixed $id the link id
+ * @param mixed $user the user data
* @return array the array with the link fields that contain an error
*/
- public function checkFields(array $data, $id = null): array
+ public function checkFields(array $data, $id, $user): array
{
$errors = [];
if (empty($data['title'])) {
- $errors['title'] = $this->language->get('invalid_value');
+ $errors[] = 'invalid_title';
}
if (!\Aurora\App\Permission::can('edit_links')) {
- http_response_code(403);
- $errors[0] = $this->language->get('no_permission');
+ $errors[] = 'no_permission';
}
return $errors;
@@ -76,6 +77,10 @@ public function getCondition(array $filters): string
{
$where = [];
+ if (isset($filters['id']) && \Aurora\Core\Helper::isValidId($filters['id'])) {
+ $where[] = 'links.id = ' . ((int) $filters['id']);
+ }
+
if (isset($filters['status']) && $filters['status'] !== '') {
$where[] = 'links.status = ' . ((int) $filters['status']);
}
@@ -96,10 +101,10 @@ public function getCondition(array $filters): string
private function getBaseData(array $data): array
{
return [
- 'title' => $data['title'],
- 'url' => $data['url'],
- 'order' => $data['order'],
- 'status' => $data['status'],
+ 'title' => $data['title'] ?? '',
+ 'url' => $data['url'] ?? '',
+ 'order' => $data['order'] ?? 0,
+ 'status' => $data['status'] ?? false,
];
}
}
diff --git a/app/controllers/modules/Page.php b/app/controllers/modules/Page.php
index bc52644..f430e08 100755
--- a/app/controllers/modules/Page.php
+++ b/app/controllers/modules/Page.php
@@ -33,38 +33,39 @@ public function add(array $data): string|bool
* Updates an existing page
* @param int $id the page id
* @param array $data the new data
+ * @param array|null $user the user performing the action
* @return bool true on success, false otherwise
*/
- public function save(int $id, array $data): bool
+ public function save(int $id, array $data, ?array $user = null): bool
{
- return $this->db->update($this->table, $this->getBaseData($data), $id) ? $id : false;
+ return $this->db->update($this->table, $this->getBaseData($data), $id);
}
/**
* Returns an array with all the page fields that contain an error
* @param array $data the page fields
- * @param [mixed] $id the page id
+ * @param mixed $id the page id
+ * @param mixed $user the user data
* @return array the array with the page fields that contain an error
*/
- public function checkFields(array $data, $id = null): array
+ public function checkFields(array $data, $id, $user): array
{
$errors = [];
if (empty($data['title'])) {
- $errors['title'] = $this->language->get('invalid_value');
+ $errors[] = 'invalid_title';
}
if (isset($data['slug']) && !empty($this->get([ 'slug' => $data['slug'], '!id' => $id ]))) {
- $errors['slug'] = $this->language->get('repeated_slug');
+ $errors[] = 'repeated_slug';
}
if (!empty($data['slug']) && !\Aurora\Core\Helper::isSlugValid($data['slug'])) {
- $errors['slug'] = $this->language->get('invalid_slug');
+ $errors[] = 'invalid_slug';
}
if (!\Aurora\App\Permission::can('edit_pages')) {
- http_response_code(403);
- $errors[0] = $this->language->get('no_permission');
+ $errors[] = 'no_permission';
}
return $errors;
@@ -79,6 +80,10 @@ public function getCondition(array $filters): string
{
$where = [];
+ if (isset($filters['id']) && \Aurora\Core\Helper::isValidId($filters['id'])) {
+ $where[] = 'pages.id = ' . ((int) $filters['id']);
+ }
+
if (isset($filters['status']) && $filters['status'] !== '') {
$where[] = 'pages.status = ' . ((int) $filters['status']);
}
@@ -99,15 +104,15 @@ public function getCondition(array $filters): string
private function getBaseData(array $data): array
{
return [
- 'title' => $data['title'],
- 'slug' => $data['slug'],
- 'html' => $data['html'],
- 'status' => $data['status'],
- 'static' => $data['static'],
- 'static_file' => $data['static_file'],
- 'meta_title' => $data['meta_title'],
- 'meta_description' => $data['meta_description'],
- 'canonical_url' => $data['canonical_url'],
+ 'title' => $data['title'] ?? '',
+ 'slug' => $data['slug'] ?? '',
+ 'html' => $data['html'] ?? '',
+ 'status' => $data['status'] ?? false,
+ 'static' => $data['static'] ?? false,
+ 'static_file' => $data['static_file'] ?? '',
+ 'meta_title' => $data['meta_title'] ?? '',
+ 'meta_description' => $data['meta_description'] ?? '',
+ 'canonical_url' => $data['canonical_url'] ?? '',
'edited_at' => time(),
];
}
diff --git a/app/controllers/modules/Post.php b/app/controllers/modules/Post.php
index 91fe2dc..a4af55a 100755
--- a/app/controllers/modules/Post.php
+++ b/app/controllers/modules/Post.php
@@ -53,9 +53,10 @@ public function add(array $data): string|bool
* Updates an existing post
* @param int $id the post id
* @param array $data the new data
+ * @param array|null $user the user performing the action
* @return bool true on success, false otherwise
*/
- public function save(int $id, array $data): bool
+ public function save(int $id, array $data, ?array $user = null): bool
{
try {
$this->db->connection->beginTransaction();
@@ -74,32 +75,32 @@ public function save(int $id, array $data): bool
/**
* Returns an array with all the post fields that contain an error
* @param array $data the post fields
- * @param [mixed] $id the post id
+ * @param mixed $id the post id
+ * @param mixed $user the user data
* @return array the array with the post fields that contain an error
*/
- public function checkFields(array $data, $id = null): array
+ public function checkFields(array $data, $id, $user): array
{
$errors = [];
if (empty($data['title'])) {
- $errors['title'] = $this->language->get('invalid_value');
+ $errors[] = 'invalid_title';
}
if (isset($data['slug']) && !empty($this->get([ 'slug' => $data['slug'], '!id' => $id ]))) {
- $errors['slug'] = $this->language->get('repeated_slug');
+ $errors[] = 'repeated_slug';
}
if (empty($data['slug']) || !\Aurora\Core\Helper::isSlugValid($data['slug'])) {
- $errors['slug'] = $this->language->get('invalid_slug');
+ $errors[] = 'invalid_slug';
}
if (!\Aurora\App\Permission::can('edit_posts')) {
- http_response_code(403);
- $errors[0] = $this->language->get('no_permission');
+ $errors[] = 'no_permission';
}
if (!empty($data['status']) && !\Aurora\App\Permission::can('publish_posts')) {
- $errors[0] = $this->language->get('published_posts_permission_error');
+ $errors[] = 'no_publish_permission';
}
return $errors;
@@ -114,12 +115,21 @@ public function getCondition(array $filters): string
{
$where = [];
+ if (isset($filters['id']) && \Aurora\Core\Helper::isValidId($filters['id'])) {
+ $where[] = 'posts.id = ' . ((int) $filters['id']);
+ }
+
if (isset($filters['status']) && $filters['status'] !== '') {
- $where[] = match (strval($filters['status'])) {
+ $val = match (strval($filters['status'])) {
'1' => 'posts.status AND posts.published_at <= ' . time(),
'0' => 'posts.status = 0',
'scheduled' => 'posts.status AND posts.published_at > ' . time(),
+ default => '',
};
+
+ if ($val) {
+ $where[] = $val;
+ }
}
if (isset($filters['user']) && $filters['user'] !== '') {
@@ -142,6 +152,13 @@ public function getCondition(array $filters): string
protected function getRowData($data): mixed
{
$data['tags'] = $this->getTags(empty($data['tags_id']) ? [] : explode(',', $data['tags_id']));
+
+ foreach ([ 'image', 'user_image' ] as $key) {
+ if (!empty($data[$key])) {
+ $data[$key] = \Aurora\Core\Helper::getContentPath($data[$key]);
+ }
+ }
+
return $data;
}
@@ -191,18 +208,18 @@ private function setTags(int $id, array $tags): void
private function getBaseData(array $data): array
{
return [
- 'title' => $data['title'],
- 'slug' => $data['slug'],
- 'description' => $data['description'],
- 'html' => $data['html'],
- 'user_id' => $data['user_id'],
- 'image' => $data['image'] ?? null,
- 'image_alt' => $data['image_alt'],
- 'status' => $data['status'],
- 'meta_title' => $data['meta_title'],
- 'meta_description' => $data['meta_description'],
- 'canonical_url' => $data['canonical_url'],
- 'published_at' => (int) strtotime($data['published_at']),
+ 'title' => $data['title'] ?? '',
+ 'slug' => $data['slug'] ?? '',
+ 'description' => $data['description'] ?? '',
+ 'html' => $data['html'] ?? '',
+ 'user_id' => $data['user_id'] ?? 0,
+ 'image' => \Aurora\Core\Helper::normalizeContentPath($data['image'] ?? null),
+ 'image_alt' => $data['image_alt'] ?? '',
+ 'status' => $data['status'] ?? false,
+ 'meta_title' => $data['meta_title'] ?? '',
+ 'meta_description' => $data['meta_description'] ?? '',
+ 'canonical_url' => $data['canonical_url'] ?? '',
+ 'published_at' => (int) ($data['published_at'] ?? 0),
];
}
}
diff --git a/app/controllers/modules/Tag.php b/app/controllers/modules/Tag.php
index 18dbf8c..1df9f1d 100755
--- a/app/controllers/modules/Tag.php
+++ b/app/controllers/modules/Tag.php
@@ -31,39 +31,40 @@ public function add(array $data): string|bool
* Updates an existing tag
* @param int $id the tag id
* @param array $data the new data
+ * @param array|null $user the user performing the action
* @return bool true on success, false otherwise
*/
- public function save(int $id, array $data): bool
+ public function save(int $id, array $data, ?array $user = null): bool
{
- return $this->db->update($this->table, $this->getBaseData($data), $id) ? $id : false;
+ return $this->db->update($this->table, $this->getBaseData($data), $id);
}
/**
* Returns an array with all the tag fields that contain an error
* @param array $data the tag fields
- * @param [mixed] $id the tag id
+ * @param mixed $id the tag id
+ * @param mixed $user the user data
* @return array the array with the tag fields that contain an error
*/
- public function checkFields(array $data, $id = null): array
+ public function checkFields(array $data, $id, $user): array
{
$errors = [];
if (empty($data['name'])) {
- $errors['name'] = $this->language->get('invalid_value');
+ $errors[] = 'invalid_name';
}
if (!empty($data['slug']) &&
!empty($this->get([ 'slug' => $data['slug'], '!id' => $id ]))) {
- $errors['slug'] = $this->language->get('repeated_slug');
+ $errors[] = 'repeated_slug';
}
if (empty($data['slug']) || !\Aurora\Core\Helper::isSlugValid($data['slug'])) {
- $errors['slug'] = $this->language->get('invalid_slug');
+ $errors[] = 'invalid_slug';
}
if (!\Aurora\App\Permission::can('edit_tags')) {
- http_response_code(403);
- $errors[0] = $this->language->get('no_permission');
+ $errors[] = 'no_permission';
}
return $errors;
@@ -78,6 +79,10 @@ public function getCondition(array $filters): string
{
$where = [];
+ if (isset($filters['id']) && \Aurora\Core\Helper::isValidId($filters['id'])) {
+ $where[] = 'tags.id = ' . ((int) $filters['id']);
+ }
+
if (!empty($filters['search'])) {
$search = $this->db->escape($filters['search']);
$where[] = "(tags.name LIKE '%$search%' OR tags.slug LIKE '%$search%')";
@@ -94,11 +99,11 @@ public function getCondition(array $filters): string
private function getBaseData(array $data): array
{
return [
- 'name' => $data['name'],
- 'slug' => $data['slug'],
- 'description' => $data['description'],
- 'meta_title' => $data['meta_title'],
- 'meta_description' => $data['meta_description'],
+ 'name' => $data['name'] ?? '',
+ 'slug' => $data['slug'] ?? '',
+ 'description' => $data['description'] ?? '',
+ 'meta_title' => $data['meta_title'] ?? '',
+ 'meta_description' => $data['meta_description'] ?? '',
];
}
}
diff --git a/app/controllers/modules/User.php b/app/controllers/modules/User.php
index 7f782eb..ac1492e 100755
--- a/app/controllers/modules/User.php
+++ b/app/controllers/modules/User.php
@@ -12,7 +12,10 @@ final class User extends \Aurora\App\ModuleBase
protected string $join = 'LEFT JOIN posts ON posts.user_id = users.id
LEFT JOIN roles ON roles.level = users.role';
protected string $group_by = 'users.id';
- protected array $relations = [ 'password_restores' => 'user_id' ];
+ protected array $relations = [
+ 'password_restores' => 'user_id',
+ 'tokens' => 'user_id',
+ ];
protected array $orders = [
'name' => 'users.name',
'email' => 'users.email',
@@ -27,22 +30,29 @@ final class User extends \Aurora\App\ModuleBase
* Updates an existing user
* @param int $id the user id
* @param array $data the new data
+ * @param array|null $user the user performing the save, its current token will be kept when revoking sessions after a password change
* @return bool true on success, false otherwise
*/
- public function save(int $id, array $data): bool
+ public function save(int $id, array $data, ?array $user = null): bool
{
$res = $this->db->update($this->table, [
'name' => $data['name'],
'slug' => $data['slug'],
'email' => $data['email'],
'status' => $data['status'],
- 'image' => $data['image'],
+ 'image' => \Aurora\Core\Helper::normalizeContentPath($data['image']),
'bio' => $data['bio'],
'role' => $data['role'],
], $id);
if ($res && !empty($data['password'])) {
$this->db->update($this->table, [ 'password' => $this->getPassword($data['password']) ], $id);
+
+ if ($id == ($user['id'] ?? 0)) {
+ $this->db->query('DELETE FROM tokens WHERE user_id = ? AND token != ?', $id, ($user['token'] ?? null));
+ } else {
+ $this->db->query('DELETE FROM tokens WHERE user_id = ?', $id);
+ }
}
return $res;
@@ -57,169 +67,104 @@ public function add(array $data): string|bool
{
$time = time();
return $this->db->insert($this->table, [
- 'name' => $data['name'],
- 'slug' => $data['slug'],
- 'email' => $data['email'],
+ 'name' => $data['name'] ?? '',
+ 'slug' => $data['slug'] ?? '',
+ 'email' => $data['email'] ?? '',
'password' => $this->getPassword($data['password']),
- 'status' => $data['status'],
- 'image' => $data['image'] ?? null,
- 'bio' => $data['bio'],
- 'role' => $data['role'],
+ 'status' => $data['status'] ?? false,
+ 'image' => \Aurora\Core\Helper::normalizeContentPath($data['image'] ?? null),
+ 'bio' => $data['bio'] ?? '',
+ 'role' => $data['role'] ?? 0,
'created_at' => $time,
'last_active' => $time,
]);
}
- /**
- * Handles the login of an user
- * @param string $email the user's email
- * @param string $password the user's password
- * @return array the array with the login errors, if empty it means the user has successfully logged in.
- */
- public function handleLogin(string $email, string $password): array
- {
- $user = $this->get([
- 'email' => $email,
- 'status' => 1,
- ]);
- $errors = [];
-
- if (!$user) {
- $errors['email'] = $this->language->get('no_active_user');
- } elseif (!password_verify($password, $user['password'])) {
- $errors['password'] = $this->language->get('wrong_password');
- }
-
- if (empty($errors)) {
- $_SESSION['user'] = $user;
- }
-
- return $errors;
- }
-
- /**
- * Sends an email to restore the password of an user
- * @param string $email the user's email
- * @param string $hash the hash to restore the password
- * @param string $message the email's content
- * @return array the array with the errors, if empty it means the email has been sent.
- */
- public function requestPasswordRestore(string $email, string $hash, string $message): array
- {
- $user = $this->get([
- 'email' => $email,
- 'status' => 1,
- ]);
- $errors = [];
-
- if (!$user) {
- $errors['email'] = $this->language->get('no_active_user');
- }
-
- if (empty($errors)) {
- $this->db->replace('password_restores', [
- 'user_id' => $user['id'],
- 'hash' => $hash,
- 'created_at' => time(),
- ]);
-
- if (!\Aurora\Core\Kernel::config('mail')($email, $this->language->get('restore_your_password'), $message)) {
- $errors['email'] = $this->language->get('error_sending_email');
- }
- }
-
- return $errors;
- }
-
- /**
- * Restores an user's password
- * @param string $hash the hash to restore the password
- * @param string $password the new password
- * @param string $password_confirm the confirmation of the new password
- * @return string the error message, if empty it means the password has been successfully been restored
- */
- public function passwordRestore(string $hash, string $password, string $password_confirm): string
- {
- $restore = $this->db->query('SELECT * FROM password_restores WHERE hash = ?', $hash)->fetch();
-
- if (empty($restore) || $restore['created_at'] < strtotime('-2 hours')) {
- return $this->language->get('error_expired_restore');
- }
-
- $error = $this->checkPassword($password, $password_confirm);
- if (empty($error)) {
- $user = $this->get([
- 'id' => $restore['user_id'],
- 'status' => 1,
- ]);
-
- if (!$user) {
- return $this->language->get('no_active_user');
- }
-
- $this->db->delete('password_restores', $hash, 'hash');
- $this->db->update($this->table, [ 'password' => $this->getPassword($password) ], $user['id']);
- $_SESSION['user'] = $user;
- }
-
- return $error;
- }
-
/**
* Returns an array with all the user fields that contain an error
* @param array $data the user fields
- * @param [mixed] $id the user id
+ * @param mixed $id the user id
+ * @param mixed $user the user data
* @return array the array with the user fields that contain an error
*/
- public function checkFields(array $data, $id = null): array
+ public function checkFields(array $data, $id, $user): array
{
$errors = [];
if (empty($data['name'])) {
- $errors['name'] = $this->language->get('invalid_value');
+ $errors[] = 'invalid_name';
}
if (!empty($data['slug']) &&
!empty($this->get([ 'slug' => $data['slug'], '!id' => $id ]))) {
- $errors['slug'] = $this->language->get('repeated_slug');
+ $errors[] = 'repeated_slug';
}
if (empty($data['slug']) || !\Aurora\Core\Helper::isSlugValid($data['slug'])) {
- $errors['slug'] = $this->language->get('invalid_slug');
+ $errors[] = 'invalid_slug';
}
if (!empty($data['email']) && !empty($this->get([ 'email' => $data['email'], '!id' => $id ]))) {
- $errors['email'] = $this->language->get('repeated_email');
+ $errors[] = 'repeated_email';
}
if (empty($data['email']) ||
filter_var($data['email'], FILTER_VALIDATE_EMAIL) === false) {
- $errors['email'] = $this->language->get('invalid_value');
+ $errors[] = 'invalid_value';
}
if (empty($id) && empty($data['password'])) {
- $errors['password'] = $this->language->get('bad_password');
+ $errors[] = 'bad_password';
}
if (!empty($data['password'])) {
- $password_error = $this->checkPassword($data['password'], $data['password_confirm']);
+ $password_error = $this->checkPassword($data['password'], $data['password_confirm'] ?? '');
if (!empty($password_error)) {
- $errors['password'] = $password_error;
+ $errors[] = $password_error;
}
}
- $can_edit = empty($id)
- ? \Aurora\App\Permission::can('edit_users')
- : \Aurora\App\Permission::edit_user($this->get([ 'id' => $id ]));
-
- if (!$can_edit) {
- http_response_code(403);
- $errors[0] = $this->language->get('no_permission');
+ if (!$user || !self::canEdit($user, \Aurora\Core\Helper::isValidId($id) ? $this->get([ 'id' => $id ]) : null, $data)) {
+ $errors[] = 'no_permission';
}
return $errors;
}
+ /**
+ * Returns true if the given actor user can edit the subject user and assign the given role
+ * @param array $current_user the user performing the action
+ * @param array|null $subject the user being edited, null when creating a new user
+ * @param array|null $new_data the new data for the subject
+ * @return bool true if the actor can edit the subject user, false otherwise
+ */
+ public static function canEdit(array $current_user, ?array $subject, ?array $new_data = null): bool
+ {
+ $current_user_role = (int) ($current_user['role'] ?? 0);
+ $is_owner = $current_user_role == 4;
+ $new_role = $new_data['role'] ?? null;
+
+ if ($subject !== null && (int) ($subject['id'] ?? 0) === (int) ($current_user['id'] ?? 0)) {
+ if (isset($new_data['status']) && (int) $new_data['status'] !== (int) ($subject['status'] ?? 0)) {
+ return false;
+ }
+
+ return $new_role === null || $new_role <= $current_user_role;
+ }
+
+ if (!\Aurora\App\Permission::can('edit_users')) {
+ return false;
+ }
+
+ if ($subject !== null && !$is_owner && (int) ($subject['role'] ?? 0) >= $current_user_role) {
+ return false;
+ }
+
+ $max_assignable_role = $is_owner ? $current_user_role : $current_user_role - 1;
+
+ return $new_role === null || $new_role <= $max_assignable_role;
+ }
+
/**
* Returns the query conditions to obtain users based on the given filters
* @param array $filters the filters
@@ -229,6 +174,10 @@ public function getCondition(array $filters): string
{
$where = [];
+ if (isset($filters['id']) && \Aurora\Core\Helper::isValidId($filters['id'])) {
+ $where[] = 'users.id = ' . ((int) $filters['id']);
+ }
+
if (isset($filters['status']) && $filters['status'] !== '') {
$where[] = 'users.status = ' . ((int) $filters['status']);
}
@@ -245,6 +194,21 @@ public function getCondition(array $filters): string
return implode(' AND ', $where);
}
+ /**
+ * Returns the user id if the given credentials are valid, false otherwise
+ * @param string $email the user email
+ * @param string $password the plain password
+ * @return int|false the user id on success, false otherwise
+ */
+ public function authenticate(string $email, string $password): int|false
+ {
+ $row = $this->db->query("SELECT id, password FROM $this->table WHERE email = ? AND status = 1", $email)->fetch();
+
+ return !$row || !password_verify($password, $row['password'])
+ ? false
+ : ((int) $row['id']);
+ }
+
/**
* Returns the given password hashed
* @param string $password the password
@@ -259,18 +223,33 @@ public function getPassword(string $password): string
* Checks the given password and its confirmation
* @param string $password the password
* @param string $password_confirm the password confirmation
- * @return string the error message, if empty it means both passwords are equal and valid
+ * @return string|false the error message, if false it means both passwords are equal and valid
*/
- private function checkPassword(string $password, string $password_confirm): string
+ public function checkPassword(string $password, string $password_confirm): string|false
{
if (mb_strlen($password) < 8) {
- return $this->language->get('bad_password');
+ return 'bad_password';
}
if ($password !== $password_confirm) {
- return $this->language->get('bad_password_confirm');
+ return 'bad_password_confirm';
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the user with additional data mapped into it
+ * @param mixed $data the user data
+ * @return mixed the user with additional data
+ */
+ protected function getRowData($data): mixed
+ {
+ if (!empty($data['image'])) {
+ $data['image'] = \Aurora\Core\Helper::getContentPath($data['image']);
}
- return '';
+ unset($data['password']);
+ return $data;
}
}
diff --git a/core/DB.php b/app/core/DB.php
similarity index 100%
rename from core/DB.php
rename to app/core/DB.php
diff --git a/core/Helper.php b/app/core/Helper.php
similarity index 53%
rename from core/Helper.php
rename to app/core/Helper.php
index 4b8520c..b10fc80 100755
--- a/core/Helper.php
+++ b/app/core/Helper.php
@@ -4,14 +4,24 @@
final class Helper
{
+ /**
+ * Returns the given path relative to the app root directory
+ * @param string $path the path
+ * @return string the given path relative to the app root directory
+ */
+ public static function getPath(string $path = ''): string
+ {
+ return dirname(__DIR__) . (empty($path) ? '' : '/' . trim($path, '/'));
+ }
+
/**
* Returns the given path relative to the project root directory
* @param string $path the path
* @return string the given path relative to the project root directory
*/
- public static function getPath(string $path = ''): string
+ public static function getProjectPath(string $path = ''): string
{
- return dirname(__DIR__) . (empty($path) ? '' : '/' . ltrim($path, '/'));
+ return dirname(self::getPath()) . (empty($path) ? '' : '/' . trim($path, '/'));
}
/**
@@ -24,6 +34,38 @@ public static function getCurrentPath(): string
return mb_substr($url, 0, mb_strpos($url, '?'));
}
+ /**
+ * Returns the path relative to the content directory with the content directory prefix
+ * @param string|null $path the path
+ * @return string|null the path with the content directory prefix
+ */
+ public static function getContentPath(?string $path = ''): ?string
+ {
+ if ($path === null || $path === '' || parse_url($path, PHP_URL_HOST)) {
+ return $path;
+ }
+
+ $content = trim(\Aurora\Core\Kernel::config('content'), '/');
+
+ return "/$content/" . ltrim($path, '/');
+ }
+
+ /**
+ * Returns the path relative to the content directory without the content directory prefix
+ * @param string|null $path the path
+ * @return string|null the path without the content directory prefix
+ */
+ public static function normalizeContentPath(?string $path): ?string
+ {
+ if ($path === null || $path === '' || parse_url($path, PHP_URL_HOST)) {
+ return $path;
+ }
+
+ $content = trim(\Aurora\Core\Kernel::config('content'), '/');
+
+ return mb_substr(ltrim($path, '/'), mb_strlen($content) + 1);
+ }
+
/**
* Returns the full url of the given path
* @param [string] $path the path
@@ -32,9 +74,16 @@ public static function getCurrentPath(): string
public static function getUrl(string $path = ''): string
{
$path = ltrim($path, '/');
- $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['SERVER_PORT'] ?? 80) == 443;
+ return 'http' . (self::isHttps() ? 's' : '') . '://' . ($_SERVER['SERVER_NAME'] ?? 'localhost') . (empty($path) ? '' : "/$path");
+ }
- return 'http' . ($https ? 's' : '') . '://' . ($_SERVER['SERVER_NAME'] ?? 'localhost') . (empty($path) ? '' : "/$path");
+ /**
+ * Returns true if the current request is made via HTTPS, false otherwise
+ * @return bool true if the current request is made via HTTPS, false otherwise
+ */
+ public static function isHttps(): bool
+ {
+ return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['SERVER_PORT'] ?? 80) == 443;
}
/**
@@ -69,6 +118,37 @@ public static function getUserIP(): mixed
return 'UNKNOWN';
}
+ /**
+ * Returns the Authorization header value from the current request
+ * @return string the Authorization header value, or an empty string if missing
+ */
+ public static function getAuthorizationHeader(): string
+ {
+ foreach ([ 'HTTP_AUTHORIZATION', 'REDIRECT_HTTP_AUTHORIZATION' ] as $key) {
+ if (!empty($_SERVER[$key])) {
+ return $_SERVER[$key];
+ }
+ }
+
+ $headers = function_exists('getallheaders') ? getallheaders() : [];
+
+ if ($headers === false) {
+ $headers = [];
+ }
+
+ if (function_exists('apache_request_headers')) {
+ $headers = array_merge($headers, apache_request_headers() ?: []);
+ }
+
+ foreach ($headers as $name => $value) {
+ if (strcasecmp($name, 'Authorization') === 0) {
+ return $value;
+ }
+ }
+
+ return '';
+ }
+
/**
* Copies the given source (file or directory) to the given destination
* @param string $source the source
@@ -79,18 +159,19 @@ public static function getUserIP(): mixed
public static function copy(string $source, string $destination, int $permission = 0755): bool
{
if (is_file($source)) {
- return copy($source, $destination);
+ mkdir(dirname($destination), $permission, true);
+ return @copy($source, $destination);
}
- if (!is_dir($destination)) {
- mkdir($destination, $permission);
+ if (!is_dir($destination) && !mkdir($destination, $permission, true)) {
+ return false;
}
$iterator = self::getFileIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $item) {
$new_path = "$destination/" . call_user_func([ $iterator, 'getSubPathname' ]);
$res = $item->isDir()
- ? (is_dir($new_path) || mkdir($new_path, $permission))
+ ? (is_dir($new_path) || mkdir($new_path, $permission, true))
: copy($item, $new_path);
if (!$res) {
@@ -149,16 +230,6 @@ public static function getPhpSize(string $size_str): int
return (int) $size;
}
- /**
- * Returns true if the given CSRF token is valid, false otherwise
- * @param string $value the CSRF token
- * @return bool true if the given CSRF token is valid, false otherwise
- */
- public static function isCsrfTokenValid(string $value): bool
- {
- return isset($_COOKIE['csrf_token']) && $_COOKIE['csrf_token'] === $value;
- }
-
/**
* Returns true if the given slug is valid
* @param string $value the slug
@@ -189,4 +260,67 @@ public static function downloadFile(string $file_path, string $filename, string
return $file_exists ? readfile($file_path) : false;
}
+
+ public static function getRequestData(): array
+ {
+ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && !empty($_POST)) {
+ return $_POST;
+ }
+
+ $raw_input = file_get_contents('php://input');
+
+ if (empty($raw_input)) {
+ return [];
+ }
+
+ $content_type = $_SERVER['CONTENT_TYPE'] ?? '';
+
+ if (stripos($content_type, 'application/json') !== false) {
+ $decoded = json_decode($raw_input, true);
+ return is_array($decoded) ? $decoded : [];
+ }
+
+ if (stripos($content_type, 'application/x-www-form-urlencoded') !== false) {
+ parse_str($raw_input, $data);
+ return $data;
+ }
+
+ return [ '_raw' => $raw_input ];
+ }
+
+ /**
+ * Appends a message to the log file
+ * @param string $message the message
+ * @param string|null $path the log file path. Defaults to the configured log file path
+ */
+ public static function log(string $message, ?string $path = null): void
+ {
+ $path ??= ini_get('error_log');
+
+ if (empty($path)) {
+ return;
+ }
+
+ file_put_contents($path, sprintf("[%s UTC] %s\n", gmdate('d-M-Y H:i:s'), $message), FILE_APPEND | LOCK_EX);
+ }
+
+ /**
+ * Removes the given directory recursively
+ * @param string $dir the directory
+ */
+ public static function removeDirRecursive(string $dir): void
+ {
+ if (!is_dir($dir)) {
+ return;
+ }
+
+ foreach (new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::CHILD_FIRST,
+ ) as $item) {
+ $item->isDir() ? rmdir($item) : unlink($item);
+ }
+
+ rmdir($dir);
+ }
}
diff --git a/core/Kernel.php b/app/core/Kernel.php
similarity index 94%
rename from core/Kernel.php
rename to app/core/Kernel.php
index 9433f7c..c954655 100644
--- a/core/Kernel.php
+++ b/app/core/Kernel.php
@@ -42,7 +42,7 @@ public function __construct(array $config)
*/
public function init(string $url): void
{
- $this->router->handleRoute($url);
+ $this->router->handleRoute($url, \Aurora\Core\Helper::getRequestData());
$this->router->handleRouteCode(http_response_code());
}
diff --git a/core/Language.php b/app/core/Language.php
similarity index 100%
rename from core/Language.php
rename to app/core/Language.php
diff --git a/core/Route.php b/app/core/Route.php
similarity index 81%
rename from core/Route.php
rename to app/core/Route.php
index 9c1deb0..7253dd2 100755
--- a/core/Route.php
+++ b/app/core/Route.php
@@ -108,15 +108,16 @@ public function code(int $code, \Closure $func): void
public function handleRouteCode(int $code): void
{
if (array_key_exists($code, $this->codes)) {
- echo $this->codes[$code]();
+ $this->outputResponse($this->codes[$code]());
}
}
/**
* Handles a url
* @param string $url the url to handle
+ * @param array $request_body the request body data
*/
- public function handleRoute(string $url): void
+ public function handleRoute(string $url, array $request_body = []): void
{
$current = array_filter(explode('/', $url));
$len = count($current) - 1;
@@ -126,7 +127,12 @@ public function handleRoute(string $url): void
if ($this->matchesRoute($current, $len, $route)) {
$this->mapParameters($current, $route);
- $middleware['action']();
+ $response = $middleware['action']($request_body);
+
+ if ($this->isResponseTuple($response)) {
+ $this->outputResponse($response);
+ return;
+ }
}
}
@@ -141,8 +147,7 @@ public function handleRoute(string $url): void
if ($this->matchesRoute($current, $len, $route)) {
$this->mapParameters($current, $route);
header('Content-Type: ' . $val['content_type']);
- http_response_code($val['status'] ?? 200);
- echo $val['action']();
+ $this->outputResponse($val['action']($request_body), $val['status'] ?? 200);
return;
}
}
@@ -241,4 +246,37 @@ private function isGet(string $str): bool
{
return preg_match(self::GET_FORMAT, $str);
}
+
+ /**
+ * Sends the route handler response.
+ * A string is used as the response body.
+ * A two-element list [body, statusCode] sets both body and HTTP status.
+ * @param mixed $response the handler return value
+ * @param int|null $default_status the HTTP status when the response is not a tuple
+ */
+ private function outputResponse(mixed $response, ?int $default_status = null): void
+ {
+ $body = $response;
+ $status = $default_status;
+
+ if ($this->isResponseTuple($response)) {
+ $body = $response[0];
+ $status = (int) $response[1];
+ }
+
+ if (isset($status)) {
+ http_response_code($status);
+ }
+
+ echo $body;
+ }
+
+ /**
+ * Returns true if the value is a [body, statusCode] response tuple
+ * @param mixed $response the handler return value
+ */
+ private function isResponseTuple(mixed $response): bool
+ {
+ return is_array($response) && array_keys($response) === [ 0, 1 ];
+ }
}
diff --git a/core/View.php b/app/core/View.php
similarity index 100%
rename from core/View.php
rename to app/core/View.php
diff --git a/app/database/fixtures.json b/app/database/fixtures.json
index c02d21e..92f87ed 100644
--- a/app/database/fixtures.json
+++ b/app/database/fixtures.json
@@ -208,22 +208,6 @@
"key": "post_code",
"value": ""
},
- {
- "key": "editor_code",
- "value": "
diff --git a/app/views/admin/base.html b/app/views/admin/base.html
deleted file mode 100755
index b72cf00..0000000
--- a/app/views/admin/base.html
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
- sectionStart('title') ?>sectionEnd() ?> - = e(setting('title')) ?>
-
- = $this->include('admin/partials/head.html') ?>
-
-
-
-
-
- = $this->include('admin/partials/snackbar.html') ?>
- sectionStart('content') ?>
- sectionEnd() ?>
-
- sectionStart('extra') ?>
- sectionEnd() ?>
-
-
diff --git a/app/views/admin/dashboard.html b/app/views/admin/dashboard.html
deleted file mode 100755
index 636fd38..0000000
--- a/app/views/admin/dashboard.html
+++ /dev/null
@@ -1,95 +0,0 @@
-extend('admin/base.html') ?>
-
-sectionStart('title') ?>
- = $this->t('dashboard') ?>
-sectionEnd() ?>
-
-sectionStart('content') ?>
-
-
-
- = $this->include('admin/partials/menu_btn.html') ?>
-
= $this->t('dashboard') ?>
-
-
-
-
-
-
-
-
= $this->t('links') ?>
-
-
-
-
-
= $this->t('latest_published_posts') ?>
-
-
-
-
-
-
= $this->t('start_creating') ?>
-
-
-
-
= $this->t('statistics') ?>
-
-
- = $this->t('posts') ?>
-
- = e($total_posts) ?> = $this->t('published') ?>,
- = e($total_scheduled_posts) ?> = $this->t('scheduled') ?>,
- = e($total_draft_posts) ?> = $this->t('draft') ?>
-
-
-
- = $this->t('pages') ?>
-
- = e($total_pages) ?> = $this->t('published') ?>,
- = e($total_draft_pages) ?> = $this->t('draft') ?>
-
-
-
- = $this->t('users') ?>
-
- = e($total_users) ?> = $this->t('active') ?>,
- = e($total_inactive_users) ?> = $this->t('inactive') ?>
-
-
-
-
-
-
-
-
-sectionEnd() ?>
diff --git a/app/views/admin/link.html b/app/views/admin/link.html
deleted file mode 100755
index fc0f746..0000000
--- a/app/views/admin/link.html
+++ /dev/null
@@ -1,84 +0,0 @@
-extend('admin/base.html') ?>
-
-sectionStart('title') ?>
- = $this->t('link') ?>
-sectionEnd() ?>
-
-sectionStart('content') ?>
-
-sectionEnd() ?>
-
-sectionStart('extra') ?>
-
-sectionEnd() ?>
diff --git a/app/views/admin/list.html b/app/views/admin/list.html
deleted file mode 100755
index 4e05631..0000000
--- a/app/views/admin/list.html
+++ /dev/null
@@ -1,88 +0,0 @@
-extend('admin/base.html') ?>
-
-sectionStart('title') ?>
- = e($title) ?>
-sectionEnd() ?>
-
-sectionStart('content') ?>
-
-
-
-
-
- = $this->include($extra_header) ?>
-
-
-
-
-sectionEnd() ?>
-
-sectionStart('extra') ?>
-
-sectionEnd() ?>
diff --git a/app/views/admin/login.html b/app/views/admin/login.html
deleted file mode 100755
index 2e2e776..0000000
--- a/app/views/admin/login.html
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
- = $this->t('sign_in') . ' - ' . e(setting('title')) ?>
- = $this->include('admin/partials/head.html') ?>
-
-
- = $this->include('admin/partials/snackbar.html') ?>
-
-
-
-
-
diff --git a/app/views/admin/page.html b/app/views/admin/page.html
deleted file mode 100755
index b309f3f..0000000
--- a/app/views/admin/page.html
+++ /dev/null
@@ -1,140 +0,0 @@
-extend('admin/base.html') ?>
-
-sectionStart('title') ?>
- = $this->t('page') ?>
-sectionEnd() ?>
-
-sectionStart('content') ?>
-
-sectionEnd() ?>
-
-sectionStart('extra') ?>
- = setting('editor_code') ?>
-
-sectionEnd() ?>
diff --git a/app/views/admin/partials/extra_headers/links.html b/app/views/admin/partials/extra_headers/links.html
deleted file mode 100644
index 6b28c6e..0000000
--- a/app/views/admin/partials/extra_headers/links.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/views/admin/partials/extra_headers/media.html b/app/views/admin/partials/extra_headers/media.html
deleted file mode 100644
index 875077c..0000000
--- a/app/views/admin/partials/extra_headers/media.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/views/admin/partials/extra_headers/pages.html b/app/views/admin/partials/extra_headers/pages.html
deleted file mode 100644
index 3cc46c3..0000000
--- a/app/views/admin/partials/extra_headers/pages.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/views/admin/partials/extra_headers/posts.html b/app/views/admin/partials/extra_headers/posts.html
deleted file mode 100644
index aa8eb4c..0000000
--- a/app/views/admin/partials/extra_headers/posts.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/views/admin/partials/extra_headers/tags.html b/app/views/admin/partials/extra_headers/tags.html
deleted file mode 100644
index 17ec383..0000000
--- a/app/views/admin/partials/extra_headers/tags.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/views/admin/partials/extra_headers/users.html b/app/views/admin/partials/extra_headers/users.html
deleted file mode 100644
index 006bae1..0000000
--- a/app/views/admin/partials/extra_headers/users.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/views/admin/partials/head.html b/app/views/admin/partials/head.html
deleted file mode 100755
index ca6b813..0000000
--- a/app/views/admin/partials/head.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
- disabled >
-
-
diff --git a/app/views/admin/partials/images_dialog.html b/app/views/admin/partials/images_dialog.html
deleted file mode 100755
index 315112a..0000000
--- a/app/views/admin/partials/images_dialog.html
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
= $this->t('image_picker') ?>
-
- = $this->include('icons/x.svg') ?>
-
-
-
-
-
-
- $file): ?>
-
-
- onclick="ImageDialog.close(); ImageDialog.setImage(= e(js($file['path'])) ?>);"
-
- onclick="ImageDialog.setImagePage(= e(js($file_path)) ?>)"
-
- >
-
-
-
-
= e(\Aurora\Core\Helper::getByteSize($file['size'])) ?>
-
-
= e($file['mime']) ?>
-
-
- = e($this->dateFormat($file['time'])) ?>
-
-
-
-
-
= $this->t('no_items') ?>
-
-
-
diff --git a/app/views/admin/partials/lists/links.html b/app/views/admin/partials/lists/links.html
deleted file mode 100755
index 81f6b60..0000000
--- a/app/views/admin/partials/lists/links.html
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
= e($link['title']) ?>
-
-
- = $link['url'] ?>
-
-
-
- = $this->t('active') ?>
-
- = $this->t('inactive') ?>
-
-
-
- = $link['order'] ?>
-
-
-
- = $this->include('icons/dots.svg') ?>
-
-
-
-
-
diff --git a/app/views/admin/partials/lists/media.html b/app/views/admin/partials/lists/media.html
deleted file mode 100644
index fc44dc5..0000000
--- a/app/views/admin/partials/lists/media.html
+++ /dev/null
@@ -1,44 +0,0 @@
- $file): ?>
-
-
-
-
-
-
= e(\Aurora\Core\Helper::getByteSize($file['size'])) ?>
-
-
= e($file['mime']) ?>
-
-
- = e($this->dateFormat($file['time'])) ?>
-
-
-
- = $this->include('icons/dots.svg') ?>
-
-
-
-
-
diff --git a/app/views/admin/partials/lists/pages.html b/app/views/admin/partials/lists/pages.html
deleted file mode 100755
index caa2a8f..0000000
--- a/app/views/admin/partials/lists/pages.html
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
- = e($page['title']) ?>
-
- = $this->t('draft') ?>
-
-
-
-
- /= e($page['slug']) ?>
-
-
- = $this->dateFormat($page['edited_at']) ?>
-
-
-
- = e($page['views']) ?>
-
-
-
-
- = $this->include('icons/dots.svg') ?>
-
-
-
-
-
diff --git a/app/views/admin/partials/lists/posts.html b/app/views/admin/partials/lists/posts.html
deleted file mode 100755
index cc47e17..0000000
--- a/app/views/admin/partials/lists/posts.html
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
![<?= e($post['image_alt'] ?? '') ?>](<?= e($this->getContentUrl($post['image'] ?? '')) ?>)
style="visibility: hidden;" />
-
-
- = e($post['title']) ?>
-
- = $this->t('draft') ?>
- time()): ?>
- = $this->t('scheduled') ?>
-
-
-
= e(implode(', ', $post['tags'])) ?>
-
-
-
- = e($post['user_name'] ?? '') ?>
-
-
- = e($this->dateFormat($post['published_at'])) ?>
-
-
-
- = e($post['views'] ? $post['views'] : '') ?>
-
-
-
-
- = $this->include('icons/dots.svg') ?>
-
-
-
-
-
diff --git a/app/views/admin/partials/lists/tags.html b/app/views/admin/partials/lists/tags.html
deleted file mode 100755
index 10f6d3c..0000000
--- a/app/views/admin/partials/lists/tags.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
= e($tag['name']) ?>
-
-
- = e($tag['slug']) ?>
-
-
- = e($tag['posts']) ?>
-
-
-
- = $this->include('icons/dots.svg') ?>
-
-
-
-
-
diff --git a/app/views/admin/partials/lists/users.html b/app/views/admin/partials/lists/users.html
deleted file mode 100755
index 674d8b8..0000000
--- a/app/views/admin/partials/lists/users.html
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
![<?= e($user['name'] ?? '') ?>](<?= e($this->getContentUrl($user['image'] ?? '')) ?>)
style="visibility: hidden;" />
-
-
-
- = e($user['name']) ?>
-
- (= $this->t('you') ?>)
-
-
- = $this->t('inactive') ?>
-
-
-
= e($user['email']) ?>
-
-
-
- = $this->t($user['role_slug']) ?>
-
-
- = e($this->dateFormat($user['last_active'])) ?>
-
-
- = e($user['posts']) ?>
-
-
-
- = $this->include('icons/dots.svg') ?>
-
-
-
-
-
diff --git a/app/views/admin/partials/media_header.html b/app/views/admin/partials/media_header.html
deleted file mode 100644
index 8f04302..0000000
--- a/app/views/admin/partials/media_header.html
+++ /dev/null
@@ -1,249 +0,0 @@
-
-
- = $this->include('admin/partials/menu_btn.html') ?>
-
-
= $this->t('media') ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
= $this->t('rename') ?>
-
- = $this->include('icons/x.svg') ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
= $this->t('create_folder') ?>
-
- = $this->include('icons/x.svg') ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
= $this->t('move') ?>
-
- = $this->include('icons/x.svg') ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
= $this->t('duplicate') ?>
-
- = $this->include('icons/x.svg') ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/views/admin/partials/menu_btn.html b/app/views/admin/partials/menu_btn.html
deleted file mode 100755
index 76b75b3..0000000
--- a/app/views/admin/partials/menu_btn.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/app/views/admin/partials/snackbar.html b/app/views/admin/partials/snackbar.html
deleted file mode 100644
index 77af6fa..0000000
--- a/app/views/admin/partials/snackbar.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
- = $this->include('icons/x-circle.svg') ?>
-
-
- = $this->include('icons/check-circle.svg') ?>
-
-
-
- = $this->include('icons/x.svg') ?>
-
-
diff --git a/app/views/admin/password_restore.html b/app/views/admin/password_restore.html
deleted file mode 100755
index aac691c..0000000
--- a/app/views/admin/password_restore.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
- = $this->t('restore_your_password') . ' - ' . e(setting('title')) ?>
- = $this->include('admin/partials/head.html') ?>
-
-
- = $this->include('admin/partials/snackbar.html') ?>
-
-
-
-
diff --git a/app/views/admin/post.html b/app/views/admin/post.html
deleted file mode 100755
index ae489e4..0000000
--- a/app/views/admin/post.html
+++ /dev/null
@@ -1,172 +0,0 @@
-extend('admin/base.html') ?>
-
-sectionStart('title') ?>
- = $this->t('post') ?>
-sectionEnd() ?>
-
-sectionStart('content') ?>
-
-sectionEnd() ?>
-
-sectionStart('extra') ?>
- = setting('editor_code') ?>
-
-sectionEnd() ?>
diff --git a/app/views/admin/settings.html b/app/views/admin/settings.html
deleted file mode 100755
index d63669e..0000000
--- a/app/views/admin/settings.html
+++ /dev/null
@@ -1,361 +0,0 @@
-extend('admin/base.html') ?>
-
-sectionStart('title') ?>
- = $this->t('settings') ?>
-sectionEnd() ?>
-
-sectionStart('content') ?>
-
-
-sectionEnd() ?>
-
-sectionStart('extra') ?>
-
-sectionEnd() ?>
diff --git a/app/views/admin/tag.html b/app/views/admin/tag.html
deleted file mode 100755
index 5b0888b..0000000
--- a/app/views/admin/tag.html
+++ /dev/null
@@ -1,97 +0,0 @@
-extend('admin/base.html') ?>
-
-sectionStart('title') ?>
- = $this->t('tag') ?>
-sectionEnd() ?>
-
-sectionStart('content') ?>
-
-sectionEnd() ?>
-
-sectionStart('extra') ?>
-
-sectionEnd() ?>
diff --git a/app/views/admin/user.html b/app/views/admin/user.html
deleted file mode 100755
index 8c7544c..0000000
--- a/app/views/admin/user.html
+++ /dev/null
@@ -1,143 +0,0 @@
-extend('admin/base.html') ?>
-
-
-t($current_user ? 'your_user' : 'user'); ?>
-
-sectionStart('title') ?>
- = $title ?>
-sectionEnd() ?>
-
-sectionStart('content') ?>
-
-sectionEnd() ?>
-
-sectionStart('extra') ?>
-
-sectionEnd() ?>
diff --git a/app/views/admin/emails/password_restore.html b/app/views/emails/password_restore.html
similarity index 72%
rename from app/views/admin/emails/password_restore.html
rename to app/views/emails/password_restore.html
index ddb0367..e200985 100644
--- a/app/views/admin/emails/password_restore.html
+++ b/app/views/emails/password_restore.html
@@ -8,8 +8,8 @@
- = e($this->url()) ?>
+ = e(\Aurora\Core\Helper::getUrl()) ?>