flush -- WIP admin panel
This commit is contained in:
parent
75af7df7b4
commit
84fb4180c9
13
README.md
13
README.md
@ -1,2 +1,11 @@
|
||||
# cavern
|
||||
A simple personal blog system.
|
||||
# Cavern
|
||||
> Explore those deep inside the cave.
|
||||
A simple blog system.
|
||||
|
||||
## Feature
|
||||
|
||||
## Requirements
|
||||
|
||||
## Install
|
||||
|
||||
## Libraries
|
223
account.php
Normal file
223
account.php
Normal file
@ -0,0 +1,223 @@
|
||||
<?php
|
||||
require_once('connection/SQL.php');
|
||||
require_once('config.php');
|
||||
require_once('include/view.php');
|
||||
require_once('include/security.php');
|
||||
|
||||
if (isset($_POST['username']) && trim($_POST['username']) != "" && isset($_POST['password']) && isset($_POST['name']) && isset($_POST['email'])) {
|
||||
// create new account
|
||||
if (!validate_csrf()) {
|
||||
http_response_code(403);
|
||||
header('axios-location: account.php?new');
|
||||
exit;
|
||||
}
|
||||
$username = $_POST['username'];
|
||||
$exist = cavern_query_result("SELECT * FROM `user` WHERE `username`='%s' OR `email`='%s'", array($username, $_POST["email"]))['num_rows'];
|
||||
if ($exist == 0) {
|
||||
if (preg_match('/^[a-z][a-z0-9\_\-]*$/', $username) && strlen($username) <= 20 && strlen($_POST['name']) <= 40 && filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$SQL->query("INSERT INTO `user` (`username`, `pwd`, `name`, `email`) VALUES ('%s', '%s', '%s', '%s')", array($username, cavern_password_hash($_POST['password'], $username), htmlspecialchars($_POST['name']), $_POST['email']));
|
||||
header('axios-location: index.php?ok=reg');
|
||||
} else {
|
||||
http_response_code(400);
|
||||
header('axios-location: index.php?err=miss');
|
||||
}
|
||||
exit;
|
||||
} else {
|
||||
http_response_code(409); // 409 Conflict
|
||||
header('axios-location: account.php?new&err=used');
|
||||
exit;
|
||||
}
|
||||
} else if (isset($_SESSION['cavern_username']) && isset($_POST['username']) && isset($_POST['old']) && (isset($_POST['name']) || isset($_POST['new']))) {
|
||||
// modify account data
|
||||
if (!validate_csrf()) {
|
||||
http_response_code(403);
|
||||
header('axios-location: account.php');
|
||||
exit;
|
||||
}
|
||||
$username = $_POST['username'];
|
||||
if ($username != $_SESSION['cavern_username']) {
|
||||
// not the same person
|
||||
http_response_code(403);
|
||||
header('axios-location: account.php?err=edit');
|
||||
exit;
|
||||
} else {
|
||||
// confirm old password and mofify account data
|
||||
$original = cavern_query_result("SELECT * FROM `user` WHERE `username`='%s'", array($username));
|
||||
if (!hash_equals(cavern_password_hash($_POST['old'], $username), $original['row']['pwd']) || $original['num_rows'] == 0) {
|
||||
http_response_code(403);
|
||||
header('axios-location: account.php?err=old');
|
||||
exit;
|
||||
} else {
|
||||
if (trim($_POST['new']) != '') {
|
||||
$password = cavern_password_hash($_POST['new'], $username);
|
||||
$SQL->query("UPDATE `user` SET `pwd`='%s' WHERE `username`='%s'", array($password, $username));
|
||||
}
|
||||
if (trim($_POST['name']) != '' && strlen($_POST['name']) <= 40) {
|
||||
$SQL->query("UPDATE `user` SET `name`='%s' WHERE `username`='%s'", array(htmlspecialchars($_POST['name']), $username));
|
||||
} else {
|
||||
http_response_code(400);
|
||||
header('axios-location: account.php?err=miss');
|
||||
exit;
|
||||
}
|
||||
if (trim($_POST['email']) != '' && filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$emailExist = cavern_query_result("SELECT * FROM `user` WHERE NOT `username`='%s' AND `email`='%s'", array($username, $_POST["email"]));
|
||||
if ($emailExist['num_rows'] == 0) {
|
||||
$SQL->query("UPDATE `user` SET `email`='%s' WHERE `username`='%s'", array($_POST['email'], $username));
|
||||
} else {
|
||||
http_response_code(400);
|
||||
header('axios-location: account.php?err=used');
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
http_response_code(400);
|
||||
header('axios-location: account.php?err=miss');
|
||||
exit;
|
||||
}
|
||||
header('axios-location: account.php?ok=edit');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
} else if (!isset($_SESSION['cavern_username']) && !isset($_GET['new'])) {
|
||||
// if mode isn't definded, redirect to register page
|
||||
header('Location: account.php?new');
|
||||
exit;
|
||||
} else if (isset($_SESSION['cavern_username']) && isset($_GET['new'])) {
|
||||
// if someone is logged in, then redirect to account setting page
|
||||
header('Location: account.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// create new account
|
||||
if (isset($_GET['new'])) {
|
||||
$view = new View('theme/default.html', 'theme/nav/default.html', 'theme/sidebar.php', $blog['name'], "註冊");
|
||||
if (!$blog['register']) {
|
||||
$view->show_message('inverted negative', "抱歉,目前暫停註冊");
|
||||
$view->render();
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['err'])) {
|
||||
if ($_GET['err'] == "miss") {
|
||||
$view->show_message('inverted negative', "請正確填寫所有欄位");
|
||||
} else if ($_GET['err'] == "used") {
|
||||
$view->show_message('inverted negative', "此使用者名稱或是信箱已被使用");
|
||||
}
|
||||
}
|
||||
|
||||
$view->add_script("./include/js/security.js");
|
||||
$view->add_script("./include/js/account.js");
|
||||
?>
|
||||
<form action="account.php" method="POST" name="newacc" autocomplete="off">
|
||||
<div class="ts form">
|
||||
<div class="ts big dividing header">註冊</div>
|
||||
<div class="required field">
|
||||
<label>帳號</label>
|
||||
<input required="required" name="username" maxlength="20" pattern="^[a-z][a-z0-9_-]*$" type="text">
|
||||
<small>上限20字元 (小寫英文、數字、底線以及連字號)。首字元必須為英文。</small>
|
||||
<small>你未來將無法更改這項設定。</small>
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label>暱稱</label>
|
||||
<input required="required" name="name" maxlength="40" type="text">
|
||||
<small>上限40字元。</small>
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label>密碼</label>
|
||||
<input required="required" name="password" type="password">
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label>重複密碼</label>
|
||||
<input required="required" name="repeat" type="password">
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label>信箱</label>
|
||||
<input required="required" name="email" type="email">
|
||||
<small>用於辨識頭貼。(Powered by <a href="https://en.gravatar.com/" target="_blank">Gravatar</a>)</small>
|
||||
</div>
|
||||
<input class="ts right floated primary button" value="送出" type="submit">
|
||||
</div>
|
||||
</form>
|
||||
<?php
|
||||
$view->render();
|
||||
} else {
|
||||
// edit account data
|
||||
$username = $_SESSION['cavern_username'];
|
||||
$result = cavern_query_result("SELECT * FROM `user` WHERE `username`='%s'", array($username));
|
||||
$name = $result['row']['name'];
|
||||
$email = $result['row']['email'];
|
||||
|
||||
$view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], "帳號");
|
||||
$view->add_script_source("ts('.ts.dropdown').dropdown();");
|
||||
$view->add_script("./include/js/security.js");
|
||||
$view->add_script("./include/js/account.js");
|
||||
|
||||
if (isset($_GET['err'])) {
|
||||
switch ($_GET['err']) {
|
||||
case 'edit':
|
||||
$view->show_message('inverted negative', "修改失敗");
|
||||
break;
|
||||
case 'old':
|
||||
$view->show_message('inverted negative', "舊密碼錯誤");
|
||||
break;
|
||||
case "miss":
|
||||
$view->show_message('inverted negative', "請正確填寫所有欄位");
|
||||
break;
|
||||
case "used":
|
||||
$view->show_message('inverted negative', "此信箱已被其他帳號使用");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isset($_GET['ok'])) {
|
||||
if ($_GET['ok'] == "edit") {
|
||||
$view->show_message('inverted positive', "修改成功!");
|
||||
}
|
||||
}
|
||||
?>
|
||||
<form action="account.php" method="POST" name="editacc">
|
||||
<div class="ts form">
|
||||
<div class="ts big dividing header">編輯帳號</div>
|
||||
<div class="fields">
|
||||
<div class="six wide field">
|
||||
<label>頭貼</label>
|
||||
<div class="ts center aligned flatted borderless segment">
|
||||
<img src="https://www.gravatar.com/avatar/<?= md5(strtolower($email)) ?>?d=https%3A%2F%2Ftocas-ui.com%2Fassets%2Fimg%2F5e5e3a6.png&s=500" class="ts rounded image" id="avatar">
|
||||
</div>
|
||||
<div data-tooltip="請透過電子信箱更換頭貼" data-tooltip-position="bottom right" class="ts top right attached label avatar tooltip">?</div>
|
||||
</div>
|
||||
<div class="ten wide field">
|
||||
<div class="disabled field">
|
||||
<label>帳號</label>
|
||||
<input type="text" name="username" value="<?= $username ?>">
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label>暱稱</label>
|
||||
<input type="text" required="required" name="name" maxlength="40" value="<?= $name ?>">
|
||||
<small>上限40字元。</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label>信箱</label>
|
||||
<input type="email" required="required" name="email" value="<?= $email ?>">
|
||||
<small>透過電子信箱,在 <a href="https://en.gravatar.com/" target="_blank">Gravatar</a> 更改你的頭貼。</small>
|
||||
</div>
|
||||
<div class="required field">
|
||||
<label>舊密碼</label>
|
||||
<input type="password" required="required" name="old">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>新密碼</label>
|
||||
<input type="password" name="new">
|
||||
<small>留空則不修改。</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>重複密碼</label>
|
||||
<input name="repeat" type="password">
|
||||
<small>重複新密碼。</small>
|
||||
</div>
|
||||
<input type="submit" class="ts right floated primary button" value="送出">
|
||||
</div>
|
||||
</form>
|
||||
<?php $view->render();
|
||||
}
|
||||
?>
|
60
admin/ajax/config.php
Normal file
60
admin/ajax/config.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
set_include_path('../../include/');
|
||||
$includepath = TRUE;
|
||||
require_once('../../config.php');
|
||||
require_once('../../connection/SQL.php');
|
||||
require_once('user.php');
|
||||
require_once('security.php');
|
||||
|
||||
$user = validate_user();
|
||||
if (!$user->valid) {
|
||||
send_error(403, "novalid");
|
||||
} else if (!($user->level >= 8)) {
|
||||
send_error(403, "nopermission");
|
||||
}
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
// modify blog settings
|
||||
if (!validate_csrf()) {
|
||||
send_error(403, "csrf");
|
||||
}
|
||||
|
||||
$config_filename = "../../config.php";
|
||||
$template_filename = "../../config.template";
|
||||
|
||||
if (!is_writable($config_filename)) {
|
||||
send_error(500, "notwritable");
|
||||
}
|
||||
|
||||
try {
|
||||
$limit = abs(intval(@$_POST["limit"]));
|
||||
$content = file_get_contents($template_filename);
|
||||
$new_content = strtr($content, array(
|
||||
"{blog_name}" => addslashes(@$_POST["name"]),
|
||||
"{limit}" => ($limit != 0 ? $limit : 10),
|
||||
"{register}" => (@$_POST["register"] === "true" ? "true" : "false")
|
||||
));
|
||||
|
||||
file_put_contents($config_filename, $new_content);
|
||||
$result = json_encode(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000)));
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
$result = json_encode(array('status' => $e->getMessage(), "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000)));
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo $result;
|
||||
exit;
|
||||
} else if ($_SERVER["REQUEST_METHOD"] == "GET") {
|
||||
// fetch settings
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array_merge(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000)), $blog));
|
||||
exit;
|
||||
}
|
||||
|
||||
function send_error($code, $message) {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array('status' => $message));
|
||||
exit;
|
||||
}
|
20
admin/ajax/statistics.php
Normal file
20
admin/ajax/statistics.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
set_include_path('../../include/');
|
||||
$includepath = TRUE;
|
||||
require_once('../../config.php');
|
||||
require_once('../../connection/SQL.php');
|
||||
require_once('user.php');
|
||||
|
||||
$user = validate_user();
|
||||
if (!$user->valid) {
|
||||
send_error(403, "novalid");
|
||||
} else if (!($user->level >= 8)) {
|
||||
send_error(403, "nopermission");
|
||||
}
|
||||
|
||||
$post_count = intval(cavern_query_result("SELECT COUNT(*) AS `count` FROM `post`")['row']['count']);
|
||||
$user_count = intval(cavern_query_result("SELECT COUNT(*) AS `count` FROM `user`")['row']['count']);
|
||||
$comment_count = intval(cavern_query_result("SELECT COUNT(*) AS `count` FROM `comment`")['row']['count']);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array("fetch" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000), "name" => $blog['name'], "post" => $post_count, "user" => $user_count, "comment" => $comment_count));
|
114
admin/ajax/user.php
Normal file
114
admin/ajax/user.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
set_include_path('../../include/');
|
||||
$includepath = TRUE;
|
||||
require_once('../../config.php');
|
||||
require_once('../../connection/SQL.php');
|
||||
require_once('user.php');
|
||||
require_once('security.php');
|
||||
|
||||
$user = validate_user();
|
||||
if (!$user->valid) {
|
||||
send_error(403, "novalid");
|
||||
} else if (!($user->level >= 8)) {
|
||||
send_error(403, "nopermission");
|
||||
}
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
// modify account data
|
||||
if (!validate_csrf()) {
|
||||
send_error(403, "csrf");
|
||||
}
|
||||
|
||||
if (isset($_POST['username']) && (isset($_POST['name']) || isset($_POST['password']))) {
|
||||
// modify account data
|
||||
$username = trim($_POST['username']);
|
||||
|
||||
try {
|
||||
$target_user = new User($username);
|
||||
} catch (NoUserException $e) {
|
||||
send_error(404, "nouser");
|
||||
}
|
||||
|
||||
if (trim($_POST['password']) != '') {
|
||||
$password = cavern_password_hash($_POST['password'], $username);
|
||||
$SQL->query("UPDATE `user` SET `pwd`='%s' WHERE `username`='%s'", array($password, $username));
|
||||
}
|
||||
if (trim($_POST['name']) != '' && strlen($_POST['name']) <= 40) {
|
||||
$SQL->query("UPDATE `user` SET `name`='%s' WHERE `username`='%s'", array(htmlspecialchars($_POST['name']), $username));
|
||||
} else {
|
||||
send_error(400, "noname");
|
||||
}
|
||||
if (trim($_POST['email']) != '' && filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$emailExist = cavern_query_result("SELECT * FROM `user` WHERE NOT `username`='%s' AND `email`='%s'", array($username, $_POST["email"]));
|
||||
if ($emailExist['num_rows'] == 0) {
|
||||
$SQL->query("UPDATE `user` SET `email`='%s' WHERE `username`='%s'", array($_POST['email'], $username));
|
||||
} else {
|
||||
send_error(400, "emailused");
|
||||
}
|
||||
} else {
|
||||
send_error(400, "noemail");
|
||||
}
|
||||
|
||||
$SQL->query("UPDATE `user` SET `muted`='%d' WHERE `username`='%s'", array($_POST['muted'], $username));
|
||||
|
||||
header("Content-Type: application/json");
|
||||
echo json_encode(array("status" => TRUE, "modified" => $username));
|
||||
exit;
|
||||
}
|
||||
} else if ($_SERVER["REQUEST_METHOD"] == "GET") {
|
||||
// fetch user list (we can fetch single user data from ajax)
|
||||
$user_list = array();
|
||||
|
||||
$user_query = cavern_query_result("SELECT * FROM `user`", array());
|
||||
if ($user_query['num_rows'] > 0) {
|
||||
do {
|
||||
$data = $user_query['row'];
|
||||
|
||||
$user_list[] = array(
|
||||
"id" => intval($data['id']),
|
||||
"username" => $data['username'],
|
||||
"name" => $data['name'],
|
||||
"email" => $data['email'],
|
||||
"role" => cavern_level_to_role($data['level'])
|
||||
);
|
||||
} while ($user_query['row'] = $user_query['query']->fetch_assoc());
|
||||
}
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000), "list" => $user_list));
|
||||
exit;
|
||||
} else if ($_SERVER["REQUEST_METHOD"] == "DELETE") {
|
||||
// delete user account
|
||||
$username = trim($_GET['username']);
|
||||
|
||||
try {
|
||||
$target_user = new User($username);
|
||||
} catch (NoUserException $e) {
|
||||
send_error(404, "nouser");
|
||||
}
|
||||
|
||||
// you cannot delete site owner
|
||||
if ($target_user->level === 9) {
|
||||
send_error(403, "deleteowner");
|
||||
}
|
||||
|
||||
/* cleanup user data */
|
||||
// Although we set foreign key, in fact `ON CASCADE` cannot fire trigger
|
||||
// like cleanup
|
||||
$SQL->query("DELETE FROM `like` WHERE `username`='%s'", array($target_user->username));
|
||||
// comment cleanup
|
||||
$SQL->query("DELETE FROM `comment` WHERE `username`='%s'", array($target_user->username));
|
||||
|
||||
// now we can delete the user data
|
||||
$SQL->query("DELETE FROM `user` WHERE `username`='%s'", array($target_user->username));
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000), "deleted" => $username));
|
||||
exit;
|
||||
}
|
||||
|
||||
function send_error($code, $message) {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array('status' => $message));
|
||||
exit;
|
||||
}
|
17
admin/component/config.js
Normal file
17
admin/component/config.js
Normal file
@ -0,0 +1,17 @@
|
||||
(() => {
|
||||
function create(tag) {
|
||||
return document.createElement(tag);
|
||||
}
|
||||
|
||||
pageManager.register("config", function () {
|
||||
return {
|
||||
render: function (...args) {
|
||||
pageManager.setHeader("設定");
|
||||
setTimeout(() => {
|
||||
pageManager.document.innerHTML = "config";
|
||||
pageManager.setLoaderState(false)
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
17
admin/component/post.js
Normal file
17
admin/component/post.js
Normal file
@ -0,0 +1,17 @@
|
||||
(() => {
|
||||
function create(tag) {
|
||||
return document.createElement(tag);
|
||||
}
|
||||
|
||||
pageManager.register("post", function () {
|
||||
return {
|
||||
render: function (...args) {
|
||||
pageManager.setHeader("文章");
|
||||
setTimeout(() => {
|
||||
pageManager.document.innerHTML = args[0] + args[1];
|
||||
pageManager.setLoaderState(false)
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
70
admin/component/statistics.js
Normal file
70
admin/component/statistics.js
Normal file
@ -0,0 +1,70 @@
|
||||
(() => {
|
||||
function fetchStatistics() {
|
||||
axios.request({
|
||||
method: "GET",
|
||||
url: "ajax/statistics.php"
|
||||
}).then(function (res) {
|
||||
renderPage(res.data);
|
||||
}).catch(function (err) {
|
||||
if (err.response) {
|
||||
console.error(err.response.status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderPage(data) {
|
||||
let iconName = {
|
||||
"post": "file text",
|
||||
"user": "users",
|
||||
"comment": "comments"
|
||||
};
|
||||
|
||||
let statLabel = {
|
||||
"post": "文章總數",
|
||||
"user": "使用者總數",
|
||||
"comment": "留言總數"
|
||||
};
|
||||
|
||||
let statTemplate = `<div class="ts left aligned statistic"><div class="value">{{ value }}</div><div class="label">{{ label }}</div></div>`
|
||||
|
||||
let cardContainer = create('div'); cardContainer.className = "ts stackable three cards";
|
||||
|
||||
for (let key in data) {
|
||||
if (Object.keys(statLabel).indexOf(key) != -1) {
|
||||
let card = create('div'); card.className = "ts card";
|
||||
let content = create('div'); content.className = "content";
|
||||
let symbol = create('div'); symbol.className = "symbol";
|
||||
let icon = create('i'); icon.className = `${iconName[key]} icon`;
|
||||
|
||||
content.innerHTML = statTemplate.replace("{{ value }}", data[key]).replace("{{ label }}", statLabel[key]);
|
||||
symbol.appendChild(icon);
|
||||
card.appendChild(content);
|
||||
card.appendChild(symbol);
|
||||
|
||||
cardContainer.appendChild(card);
|
||||
} else if (key === "name") {
|
||||
// blog name
|
||||
}
|
||||
}
|
||||
|
||||
// finish up
|
||||
setTimeout(() => {
|
||||
pageManager.document.innerHTML = "";
|
||||
pageManager.document.appendChild(cardContainer);
|
||||
pageManager.setLoaderState(false)
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function create(tag) {
|
||||
return document.createElement(tag);
|
||||
}
|
||||
|
||||
pageManager.register("statistics", function () {
|
||||
return {
|
||||
render: function (...args) {
|
||||
pageManager.setHeader("總覽");
|
||||
fetchStatistics();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
17
admin/component/user.js
Normal file
17
admin/component/user.js
Normal file
@ -0,0 +1,17 @@
|
||||
(() => {
|
||||
function create(tag) {
|
||||
return document.createElement(tag);
|
||||
}
|
||||
|
||||
pageManager.register("user", function () {
|
||||
return {
|
||||
render: function (...args) {
|
||||
pageManager.setHeader("使用者");
|
||||
setTimeout(() => {
|
||||
pageManager.document.innerHTML = args[0];
|
||||
pageManager.setLoaderState(false)
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
114
admin/dashboard.js
Normal file
114
admin/dashboard.js
Normal file
@ -0,0 +1,114 @@
|
||||
$(document).ready(function () {
|
||||
var $sidebar = $('.ts.sidebar');
|
||||
let mq = window.matchMedia("(max-width: 768px)");
|
||||
mq.addListener(sidebarOnMobile);
|
||||
|
||||
sidebarOnMobile(mq); // run first
|
||||
function sidebarOnMobile(q) {
|
||||
if (q.matches) {
|
||||
/* mobile -> hide sidebar */
|
||||
$sidebar.toggleClass("animating", true);
|
||||
$sidebar.toggleClass("visible", false);
|
||||
} else {
|
||||
/* non-mobile -> show sidebar */
|
||||
$sidebar.toggleClass("animating", false);
|
||||
$sidebar.toggleClass("visible", true);
|
||||
}
|
||||
}
|
||||
|
||||
$('button#toggleSidebar').on('click', function (e) {
|
||||
$sidebar.toggleClass('visible');
|
||||
});
|
||||
});
|
||||
|
||||
function Manager(element) {
|
||||
this.components = {};
|
||||
this.cache = {}; // args cache for components just be on load
|
||||
|
||||
this.document = element;
|
||||
}
|
||||
|
||||
Manager.prototype = {
|
||||
load: function(name, ...args) {
|
||||
this.setLoaderState(true);
|
||||
if (Object.keys(this.components).indexOf(name) == -1) {
|
||||
// cache args
|
||||
this.cache[name] = args;
|
||||
script = document.createElement("script");
|
||||
script.src = `component/${name}.js`;
|
||||
document.body.appendChild(script);
|
||||
} else {
|
||||
this.components[name].render(...args);
|
||||
}
|
||||
},
|
||||
register: function(name, init) {
|
||||
this.components[name] = init();
|
||||
let args = this.cache[name];
|
||||
this.components[name].render(...args);
|
||||
delete this.cache[name];
|
||||
},
|
||||
setHeader: function (title) {
|
||||
$('#header').text(title);
|
||||
},
|
||||
setLoaderState: function (state) {
|
||||
$('.pusher .dimmer').toggleClass('active', state);
|
||||
}
|
||||
}
|
||||
|
||||
let pageManager = new Manager(document.querySelector('#content'));
|
||||
|
||||
var root = "./", useHash = true, hash = "#";
|
||||
let router = new Navigo(root, useHash, hash);
|
||||
|
||||
if (location.href.slice(-1) === "/") {
|
||||
// catch to navigo
|
||||
router.navigate("/");
|
||||
}
|
||||
|
||||
router.on({
|
||||
"/": function () {
|
||||
// system overview
|
||||
render("statistics");
|
||||
},
|
||||
"/post": function () {
|
||||
render("post", "page", 1);
|
||||
},
|
||||
"/post/:pid": function (params) {
|
||||
render("post", "pid", params.pid);
|
||||
},
|
||||
"/post/page/:page": function (params) {
|
||||
render("post", "page", params.page);
|
||||
},
|
||||
"/user": function () {
|
||||
render("user", "list");
|
||||
},
|
||||
"/user/add": function () {
|
||||
render("user", "add");
|
||||
},
|
||||
"/user/:username": function (params) {
|
||||
render("user", "username", params.username);
|
||||
},
|
||||
"/config": function () {
|
||||
render("config");
|
||||
}
|
||||
}).resolve();
|
||||
|
||||
router.updatePageLinks();
|
||||
|
||||
function render(page, ...args) {
|
||||
switch (page) {
|
||||
case "user":
|
||||
pageManager.load("user", ...args);
|
||||
break;
|
||||
case "post":
|
||||
pageManager.load("post", ...args);
|
||||
break;
|
||||
case "config":
|
||||
pageManager.load("config", ...args);
|
||||
break;
|
||||
case "statistics":
|
||||
default:
|
||||
pageManager.load("statistics", ...args);
|
||||
break;
|
||||
}
|
||||
}
|
28
admin/index.php
Normal file
28
admin/index.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
set_include_path('../include/');
|
||||
$includepath = TRUE;
|
||||
require_once('../connection/SQL.php');
|
||||
require_once('../config.php');
|
||||
require_once('view.php');
|
||||
require_once('security.php');
|
||||
require_once('user.php');
|
||||
|
||||
$user = validate_user();
|
||||
if (!$user->valid) {
|
||||
http_response_code(403);
|
||||
header("Location: ../index.php?err=account");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$user->islogin) {
|
||||
http_response_code(401);
|
||||
header('Location: ../login.php?next=admin');
|
||||
exit;
|
||||
} else if ($user->level < 8) {
|
||||
http_response_code(403);
|
||||
header('Location: ../index.php?err=permission');
|
||||
exit;
|
||||
}
|
||||
|
||||
$view = new View('./theme/dashboard.html', 'theme/avatar.php', '', $blog['name'], "管理介面");
|
||||
$view->render();
|
38
admin/theme/admin.css
Normal file
38
admin/theme/admin.css
Normal file
@ -0,0 +1,38 @@
|
||||
/* nav bar */
|
||||
button#toggleSidebar {
|
||||
border-radius: 0;
|
||||
border-bottom-right-radius: .28571rem;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.pusher > nav {
|
||||
z-index: 14;
|
||||
position: sticky;
|
||||
position: -webkit-sticky; /* safari */
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* content */
|
||||
#header {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ts.visible.sidebar:not(.overlapped) ~ .pusher.squeezable {
|
||||
/* RWD fix */
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
button#toggleSidebar {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.visible.sidebar ~ .pusher > nav {
|
||||
width: calc(100vw - 230px);
|
||||
transform: translate3d(230px, 0, 0);
|
||||
transition: transform .45s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
}
|
12
admin/theme/avatar.php
Normal file
12
admin/theme/avatar.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
require_once('../config.php');
|
||||
require_once('../connection/SQL.php');
|
||||
require_once('../include/user.php');
|
||||
|
||||
$user = validate_user();
|
||||
?>
|
||||
<div class="center aligned item">
|
||||
<img class="ts tiny circular image" src="https://www.gravatar.com/avatar/<?= md5($user->email) ?>?d=https%3A%2F%2Ftocas-ui.com%2Fassets%2Fimg%2F5e5e3a6.png&s=150">
|
||||
<br><br>
|
||||
<div><?= $user->name ?></div>
|
||||
</div>
|
69
admin/theme/dashboard.html
Normal file
69
admin/theme/dashboard.html
Normal file
@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Tocas UI:CSS 與元件 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/2.3.3/tocas.css">
|
||||
<!-- Tocas JS:模塊與 JavaScript 函式 -->
|
||||
<script src="../include/js/lib/tocas.js"></script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@7.26.11/dist/sweetalert2.all.min.js"></script>
|
||||
<link rel="stylesheet" href="theme/admin.css">
|
||||
<title>{part} | {title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts left vertical fluid inverted visible menu sidebar">
|
||||
{nav}
|
||||
|
||||
<div class="item">
|
||||
<i class="users icon"></i> 使用者
|
||||
<div class="menu">
|
||||
<a class="item" href="/user" data-navigo>列表</a>
|
||||
<a class="item" href="/user/add" data-navigo>新增</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<i class="file text icon"></i> 文章
|
||||
<div class="menu">
|
||||
<a href="/post" class="item" data-navigo>列表</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<i class="setting icon"></i> 系統
|
||||
<div class="menu">
|
||||
<a class="item" href="/" data-navigo>總覽</a>
|
||||
<a class="item" href="/config" data-navigo>設定</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom menu">
|
||||
<a href="../" class="item"><i class="arrow left icon"></i>返回部落格</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comupter squeezable pusher" id="pusher">
|
||||
<div class="ts active inverted dimmer">
|
||||
<div class="ts loader"></div>
|
||||
</div>
|
||||
<nav class="sidebar menu">
|
||||
<button class="ts inverted icon button" id="toggleSidebar">
|
||||
<i class="sidebar icon"></i>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="ts narrow container">
|
||||
<div class="ts big dividing header" id="header"></div>
|
||||
<div class="ts fluid container" id="content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
<script src="https://unpkg.com/navigo@6"></script>
|
||||
<script src="../include/js/security.js"></script>
|
||||
<script src="dashboard.js"></script>
|
||||
{script}
|
||||
</body>
|
||||
</html>
|
238
ajax/comment.php
Normal file
238
ajax/comment.php
Normal file
@ -0,0 +1,238 @@
|
||||
<?php
|
||||
set_include_path('../include/');
|
||||
$includepath = TRUE;
|
||||
require_once('../connection/SQL.php');
|
||||
require_once('../config.php');
|
||||
require_once('security.php');
|
||||
require_once('user.php');
|
||||
require_once('article.php');
|
||||
require_once('notification.php');
|
||||
|
||||
$user = validate_user();
|
||||
if (!$user->valid) {
|
||||
http_response_code(403);
|
||||
header("Content-Type: applcation/json");
|
||||
echo json_encode(array('status' => 'novalid'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($_GET['pid']) && !isset($_GET['del']) && !isset($_POST['pid']) && !isset($_POST['edit'])) {
|
||||
send_error(404, "error");
|
||||
} else {
|
||||
if (isset($_GET['pid']) && trim($_GET['pid']) != "") {
|
||||
if (isset($_SESSION['cavern_comment_time']) && $_SERVER['REQUEST_TIME'] - $_SESSION['cavern_comment_time'] > 10) {
|
||||
// after 10 seconds
|
||||
$_SESSION['cavern_comment_time'] = NULL;
|
||||
unset($_SESSION['cavern_comment_time']);
|
||||
}
|
||||
$data = process_comments($_GET['pid']);
|
||||
} else {
|
||||
if (!$user->islogin) { // guest
|
||||
send_error(401, "nologin");
|
||||
}
|
||||
if (!validate_csrf()) {
|
||||
send_error(403, "csrf");
|
||||
}
|
||||
|
||||
if (isset($_GET['del']) && trim($_GET['del']) != "") {
|
||||
// delete comment
|
||||
$result = cavern_query_result("SELECT * FROM `comment` WHERE `id`='%d'", array($_GET['del']));
|
||||
if ($result['num_rows'] < 1) {
|
||||
send_error(404, "error");
|
||||
}
|
||||
|
||||
$author = $result['row']['username'];
|
||||
|
||||
if ($author !== $user->username) {
|
||||
send_error(403, false);
|
||||
}
|
||||
|
||||
$SQL->query("DELETE FROM `comment` WHERE `id`='%d' AND `username`='%s'", array($_GET['del'], $user->username));
|
||||
$data = array(
|
||||
"status" => TRUE,
|
||||
"time" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000)
|
||||
);
|
||||
} else if (isset($_POST['content'])) {
|
||||
if (isset($_POST['pid']) && trim($_POST['pid']) && isset($_SESSION['cavern_comment_time']) && $_SERVER['REQUEST_TIME'] - $_SESSION['cavern_comment_time'] < 10) {
|
||||
// user can create one comment per 10 seconds
|
||||
$remain_second = 10 - ($_SERVER['REQUEST_TIME'] - $_SESSION['cavern_comment_time']);
|
||||
header('Retry-After: ' . $remain_second);
|
||||
send_error(429, "ratelimit");
|
||||
}
|
||||
|
||||
if ($user->muted) {
|
||||
send_error(403, "muted");
|
||||
}
|
||||
|
||||
if (trim($_POST['content']) != "") {
|
||||
if (isset($_POST['pid']) && trim($_POST['pid']) != "") {
|
||||
// new comment
|
||||
try {
|
||||
$article = new Article(intval($_POST['pid']));
|
||||
} catch (NoPostException $e) {
|
||||
send_error(404, "error");
|
||||
}
|
||||
|
||||
http_response_code(201); // 201 Created
|
||||
$time = date('Y-m-d H:i:s');
|
||||
$SQL->query("INSERT INTO `comment` (`pid`, `username`, `time`, `content`) VALUES ('%d', '%s', '%s', '%s')", array($_POST['pid'], $user->username, $time, htmlspecialchars($_POST['content'])));
|
||||
$comment_id = $SQL->insert_id();
|
||||
$data = array(
|
||||
"status" => TRUE,
|
||||
"comment_id" => $comment_id,
|
||||
"time" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000)
|
||||
);
|
||||
|
||||
/* notification */
|
||||
|
||||
// notify tagged user
|
||||
// the user who tag himself is unnecessary to notify
|
||||
$username_list = parse_user_tag($_POST['content']);
|
||||
foreach ($username_list as $key => $id) {
|
||||
if ($id == $user->username) continue;
|
||||
cavern_notify_user($id, "{{$user->name}}@{$user->username} 在 [{$article->title}] 的留言中提到了你", "post.php?pid={$article->pid}#comment-$comment_id", "comment");
|
||||
}
|
||||
|
||||
// notify commenters
|
||||
$commenters = cavern_query_result("SELECT `username` FROM `comment` WHERE `pid` = '%d'", array($_POST['pid']));
|
||||
if ($commenters['num_rows'] > 0) {
|
||||
do {
|
||||
$u = $commenters['row']['username'];
|
||||
if (!in_array($u, $username_list) && $u != $article->author && $u != $user->username) {
|
||||
cavern_notify_user($u, "在你回應的文章 [{$article->title}] 中有了新的回應", "post.php?pid={$article->pid}#comment-$comment_id", "comment");
|
||||
}
|
||||
} while ($commenters['row'] = $commenters['query']->fetch_assoc());
|
||||
}
|
||||
|
||||
// notify liked user
|
||||
/* we won't inform the author for his like on his own post
|
||||
and no notice for his own comment */
|
||||
$likers = cavern_query_result("SELECT `username` FROM `like` WHERE `pid` = '%d'", array($_POST['pid']));
|
||||
if ($likers['num_rows'] > 0) {
|
||||
do {
|
||||
$u = $likers['row']['username'];
|
||||
if (!in_array($u, $username_list) && $u != $article->author && $u != $user->username) {
|
||||
cavern_notify_user($u, "在你喜歡的文章 [{$article->title}] 中有了新的回應", "post.php?pid={$article->pid}#comment-$comment_id", "comment");
|
||||
}
|
||||
} while ($likers['row'] = $likers['query']->fetch_assoc());
|
||||
}
|
||||
|
||||
// notify post author
|
||||
/* we won't inform the author if he has been notified for being tagged
|
||||
also, we won't notify the author for his own comment */
|
||||
if (!in_array($article->author, $username_list) && $article->author != $user->username) {
|
||||
cavern_notify_user($article->author, "{{$user->name}}@{$user->username} 回應了 [{$article->title}]", "post.php?pid={$article->pid}#comment-$comment_id", "comment");
|
||||
}
|
||||
|
||||
// only new comment should be limited
|
||||
$_SESSION['cavern_comment_time'] = $_SERVER['REQUEST_TIME'];
|
||||
} else if (isset($_POST['edit']) && trim($_POST['edit']) != "") {
|
||||
// edit comment
|
||||
$query = cavern_query_result("SELECT * FROM `comment` WHERE `id` = '%d'", array($_POST['edit']));
|
||||
if ($query['num_rows'] < 1) {
|
||||
send_error(404, "error");
|
||||
}
|
||||
if ($query['row']['username'] !== $user->username) {
|
||||
send_error(403, "author");
|
||||
}
|
||||
|
||||
$time = date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']);
|
||||
$SQL->query("UPDATE `comment` SET `content`='%s', `modified`='%s' WHERE `id`='%d' AND `username`='%s'", array(htmlspecialchars($_POST['content']), $time, $_POST['edit'], $user->username));
|
||||
$data = array(
|
||||
"status" => TRUE,
|
||||
"comment_id" => $_POST['edit'],
|
||||
"time" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000)
|
||||
);
|
||||
} else {
|
||||
send_error(400, "empty");
|
||||
}
|
||||
} else {
|
||||
send_error(400, "empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
|
||||
function process_comments($pid) {
|
||||
if (isset($_SESSION["cavern_username"])) {
|
||||
$user = new User($_SESSION["cavern_username"]);
|
||||
} else {
|
||||
$user = new User(""); // guest
|
||||
}
|
||||
|
||||
if (cavern_query_result("SELECT * FROM `post` WHERE `pid`=%d", array($pid))['num_rows'] < 1) {
|
||||
http_response_code(404);
|
||||
$json = array('status' => 'error', 'fetch' => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000)); // to fit javascript unit
|
||||
return $json;
|
||||
}
|
||||
|
||||
if (isset($_COOKIE['cavern_commentLastFetch'])) {
|
||||
$last_fetch_time = $_COOKIE['cavern_commentLastFetch'];
|
||||
}
|
||||
|
||||
$email_hash = array();
|
||||
$names = array();
|
||||
$id_list = array();
|
||||
$modified = array();
|
||||
$comments = array();
|
||||
$result = cavern_query_result("SELECT * FROM `comment` WHERE `pid`='%d'", array($pid));
|
||||
$json = array('status' => TRUE, 'fetch' => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000)); // to fit javascript unit
|
||||
|
||||
if ($result['num_rows'] > 0) {
|
||||
do {
|
||||
$username = $result['row']['username'];
|
||||
if (!isset($names[$username])) {
|
||||
$target_user = new User($username);
|
||||
$name = $target_user->name;
|
||||
$email = $target_user->email;
|
||||
|
||||
$names[$username] = $name;
|
||||
$email_hash[$username] = md5(strtolower($email));
|
||||
}
|
||||
|
||||
$comment = array(
|
||||
"id" => $result['row']['id'],
|
||||
"username" => $username,
|
||||
"markdown" => $result['row']['content'],
|
||||
"time" => $result['row']['time'],
|
||||
"modified" => (is_null($result['row']['modified']) ? FALSE : $result['row']['modified'])
|
||||
// if the comment has been modified, set this value as modified time; otherwise, set to FALSE
|
||||
);
|
||||
|
||||
if ($user->islogin && $user->username === $username) {
|
||||
$comment['actions'] = array("reply", "edit", "del");
|
||||
} else if ($user->islogin) {
|
||||
$comment['actions'] = array("reply");
|
||||
} else {
|
||||
$comment['actions'] = array();
|
||||
}
|
||||
$id_list[] = $comment['id']; // append id
|
||||
$comments[] = $comment; // append comment
|
||||
|
||||
if (!is_null($result['row']['modified']) && isset($last_fetch_time)) {
|
||||
if (strtotime($result['row']['modified']) - $last_fetch_time > 0) {
|
||||
$modified[] = $comment["id"];
|
||||
}
|
||||
}
|
||||
} while ($result['row'] = $result['query']->fetch_assoc());
|
||||
}
|
||||
$json['idList'] = $id_list;
|
||||
$json['modified'] = $modified;
|
||||
$json['comments'] = $comments;
|
||||
$json['names'] = $names;
|
||||
$json['hash'] = $email_hash;
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
function send_error($code, $message) {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array('status' => $message, 'fetch' => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000))); // to fit javascript timestamp
|
||||
exit;
|
||||
}
|
||||
?>
|
109
ajax/like.php
Normal file
109
ajax/like.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
set_include_path('../include/');
|
||||
$includepath = TRUE;
|
||||
require_once('../connection/SQL.php');
|
||||
require_once('../config.php');
|
||||
require_once('user.php');
|
||||
require_once('security.php');
|
||||
require_once('notification.php');
|
||||
|
||||
$user = validate_user();
|
||||
if (!$user->valid) {
|
||||
http_response_code(403);
|
||||
header("Content-Type: applcation/json");
|
||||
echo json_encode(array('status' => 'novalid'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($_GET['pid'])) {
|
||||
http_response_code(404);
|
||||
$data = array('status' => 'error');
|
||||
} else {
|
||||
$pid = $_GET['pid'];
|
||||
|
||||
$article = cavern_query_result("SELECT * FROM `post` WHERE `pid`='%d'", array($pid));
|
||||
if ($article['num_rows'] < 1) {
|
||||
http_response_code(404);
|
||||
echo json_encode(array('status' => 'nopost', 'id' => $pid));
|
||||
exit;
|
||||
}
|
||||
|
||||
$likes_query = process_like($pid, $user);
|
||||
|
||||
$islike = $likes_query[0];
|
||||
$likes = $likes_query[1];
|
||||
$likers = $likes_query[2];
|
||||
|
||||
if (isset($_GET['fetch'])) {
|
||||
// fetch likes
|
||||
$data = array('status' => 'fetch', 'id' => $pid, 'likes' => $likes, 'likers' => $likers);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
} else if (!$user->islogin) {
|
||||
// ask guest to login
|
||||
$data = array('status' => 'nologin', 'id' => $pid, 'likes' => $likes, 'likers' => $likers);
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
} else {
|
||||
// user like actions
|
||||
if (!validate_csrf()) { // csrf attack!
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array('status' => 'csrf', 'id' => $pid, 'likes' => $likes, 'likers' => $likers));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($islike) {
|
||||
// unlike
|
||||
$SQL->query("DELETE FROM `like` WHERE `pid`='%d' AND `username`='%s'", array($pid, $user->username));
|
||||
$result = process_like($pid, $user);
|
||||
$likes = $result[1];
|
||||
$likers = $result[2];
|
||||
$data = array('status' => FALSE, 'id' => $pid, 'likes' => $likes, 'likers' => $likers);
|
||||
} else {
|
||||
// like
|
||||
$SQL->query("INSERT INTO `like` (`pid`, `username`) VALUES ('%d', '%s')", array($pid, $user->username));
|
||||
$result = process_like($pid, $user);
|
||||
$likes = $result[1];
|
||||
$likers = $result[2];
|
||||
$data = array('status' => TRUE, 'id' => $pid, 'likes' => $likes, 'likers' => $likers);
|
||||
|
||||
/* notification */
|
||||
// notify article author
|
||||
// we should notify author this only once
|
||||
$author = $article['row']['username'];
|
||||
$notification_query = cavern_query_result("SELECT * FROM `notification` WHERE `username`='%s' AND `url`='%s' AND `type`='%s'", array($author, "post.php?pid=$pid", "like"));
|
||||
if (!($notification_query['num_rows'] > 0) && $user->username !== $author) {
|
||||
cavern_notify_user($author, "{{$user->name}}@{$user->username} 推了 [{$article['row']['title']}]", "post.php?pid=$pid", "like");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function process_like($pid, $user) {
|
||||
$islike = false;
|
||||
$likers = array();
|
||||
$likes_query = cavern_query_result("SELECT * FROM `like` WHERE `pid`='%d'", array($pid));
|
||||
|
||||
if ($likes_query['num_rows'] < 1){
|
||||
$likes = 0;
|
||||
} else {
|
||||
$likes = $likes_query['num_rows'];
|
||||
do {
|
||||
$likers[] = $likes_query['row']['username'];
|
||||
if ($user->username === $likes_query['row']['username']) {
|
||||
$islike = true;
|
||||
}
|
||||
} while ($likes_query['row'] = $likes_query['query']->fetch_assoc());
|
||||
}
|
||||
|
||||
return array($islike, $likes, array_unique($likers));
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
?>
|
51
ajax/notification.php
Normal file
51
ajax/notification.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
set_include_path('../include/');
|
||||
$includepath = TRUE;
|
||||
require_once('../include/security.php');
|
||||
require_once('../connection/SQL.php');
|
||||
require_once('../config.php');
|
||||
|
||||
if (isset($_GET['fetch']) || isset($_GET['count'])) {
|
||||
if (isset($_SESSION['cavern_username'])) {
|
||||
if (isset($_GET['fetch'])) {
|
||||
$data = process_notifications(20); // fetch 20 comments
|
||||
$SQL->query("UPDATE `notification` SET `read` = 1 WHERE `read` = 0 AND `username` = '%s'", array($_SESSION['cavern_username'])); // read all comments
|
||||
} else if (isset($_GET['count'])) {
|
||||
$query = cavern_query_result("SELECT COUNT(*) AS `count` FROM `notification` WHERE `username` = '%s' AND `read` = 0", array($_SESSION['cavern_username']));
|
||||
$count = $query['row']['count'];
|
||||
$data = array("status" => TRUE, "fetch" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000), "unread_count" => $count);
|
||||
}
|
||||
} else {
|
||||
send_error(401, "nologin");
|
||||
}
|
||||
} else {
|
||||
send_error(404, "error");
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
|
||||
function process_notifications($limit) {
|
||||
$result = cavern_query_result("SELECT * FROM `notification` WHERE `username` = '%s' ORDER BY `time` DESC LIMIT %d" ,array($_SESSION['cavern_username'], $limit));
|
||||
$json = array('status' => TRUE, 'fetch' => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000)); // to fit javascript unit
|
||||
|
||||
$feeds = array();
|
||||
|
||||
if ($result['num_rows'] > 0) {
|
||||
do {
|
||||
$feeds[] = $result['row'];
|
||||
} while ($result['row'] = $result['query']->fetch_assoc());
|
||||
}
|
||||
|
||||
$json['feeds'] = $feeds;
|
||||
return $json;
|
||||
}
|
||||
|
||||
function send_error($code, $message) {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array('status' => $message, 'fetch' => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000))); // to fit javascript timestamp
|
||||
exit;
|
||||
}
|
||||
?>
|
107
ajax/posts.php
Normal file
107
ajax/posts.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
set_include_path('../include/');
|
||||
$includepath = TRUE;
|
||||
require_once('../connection/SQL.php');
|
||||
require_once('../config.php');
|
||||
require_once('user.php');
|
||||
require_once('article.php');
|
||||
|
||||
$data = array("fetch" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000));
|
||||
|
||||
$user = validate_user();
|
||||
if (!$user->valid) {
|
||||
$data["status"] = "invalid";
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['pid'])) {
|
||||
// get data of single post
|
||||
$pid = abs($_GET['pid']);
|
||||
try {
|
||||
$article = new Article($pid);
|
||||
} catch (NoPostException $e) {
|
||||
// post not found
|
||||
http_response_code(404);
|
||||
$data['message'] = $e->getMessage();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
$post = array(
|
||||
'author' => $article->author,
|
||||
'name' => $article->name,
|
||||
'title' => $article->title,
|
||||
'content' => $article->content,
|
||||
'time' => $article->time,
|
||||
'likes_count' => $article->likes_count,
|
||||
'comments_count' => $article->comments_count,
|
||||
'islike' => $article->is_like($user)
|
||||
);
|
||||
|
||||
$data['post'] = $post;
|
||||
} else {
|
||||
// get posts list
|
||||
if (isset($_GET['limit']) && trim($_GET['limit']) != ""){
|
||||
$limit = intval($_GET['limit']);
|
||||
} else {
|
||||
$limit = intval($blog['limit']);
|
||||
}
|
||||
|
||||
if (isset($_GET['page']) && trim($_GET['page']) != "") {
|
||||
$page = intval($_GET['page']);
|
||||
$limit_start = abs(($page - 1) * $limit);
|
||||
} else if (isset($_GET['username']) && trim($_GET['username']) != "") {
|
||||
$mode = "username";
|
||||
} else {
|
||||
$page = 1;
|
||||
$limit_start = 0;
|
||||
}
|
||||
|
||||
if (isset($mode) && $mode == "username") {
|
||||
$post_list = article_list(cavern_query_result(
|
||||
"SELECT `post`.*, `user`.name FROM `post` INNER JOIN `user` ON `post`.username = `user`.username WHERE `post`.username = '%s' ORDER BY `time`",
|
||||
array($_GET['username'])
|
||||
));
|
||||
$all_posts_count = cavern_query_result("SELECT COUNT(*) AS `count` FROM `post` WHERE `username` = '%s'", array($_GET['username']))['row']['count'];
|
||||
} else {
|
||||
$post_list = article_list(cavern_query_result(
|
||||
"SELECT `post`.*, `user`.name FROM `post` INNER JOIN `user` ON `post`.username = `user`.username ORDER BY `time` DESC LIMIT %d,%d",
|
||||
array($limit_start, $limit)
|
||||
));
|
||||
$all_posts_count = cavern_query_result("SELECT COUNT(*) AS `count` FROM `post`")['row']['count'];
|
||||
|
||||
$data['page_limit'] = $limit;
|
||||
$data['page'] = $page;
|
||||
}
|
||||
|
||||
$data['all_posts_count'] = intval($all_posts_count);
|
||||
|
||||
$posts = array();
|
||||
|
||||
foreach ($post_list as $_key => $article) {
|
||||
$post = array(
|
||||
'username' => $article->author,
|
||||
'name' => $article->name,
|
||||
'pid' => $article->pid,
|
||||
'title' => $article->title,
|
||||
'content' => $article->content,
|
||||
'time' => $article->time,
|
||||
'likes_count' => $article->likes_count,
|
||||
'comments_count' => $article->comments_count,
|
||||
'islike' => $article->is_like($user)
|
||||
);
|
||||
|
||||
$posts[] = $post; // append post
|
||||
}
|
||||
|
||||
$data["posts"] = $posts;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
?>
|
60
ajax/user.php
Normal file
60
ajax/user.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
set_include_path('../include/');
|
||||
$includepath = TRUE;
|
||||
require_once('../connection/SQL.php');
|
||||
require_once('../config.php');
|
||||
require_once('user.php');
|
||||
|
||||
$user = validate_user();
|
||||
if (!$user->valid) {
|
||||
send_error(403, "invalid", $user->islogin);
|
||||
}
|
||||
|
||||
if (isset($_GET['username']) && trim($_GET['username']) != "") {
|
||||
// query other user's profile
|
||||
$username = trim($_GET['username']);
|
||||
} else if ($user->islogin) {
|
||||
// query the profile of the user himself
|
||||
$username = $user->username;
|
||||
} else {
|
||||
// username isn't provided
|
||||
send_error(404, "error");
|
||||
}
|
||||
|
||||
try {
|
||||
$target_user = new User($username);
|
||||
} catch (NoUserException $_e) {
|
||||
send_error(404, "nouser", $user->islogin);
|
||||
}
|
||||
|
||||
$posts = cavern_query_result("SELECT * FROM `post` WHERE `username`='%s'", array($username));
|
||||
$posts_count = ($posts['num_rows'] > 0 ? $posts['num_rows'] : 0);
|
||||
|
||||
$data = array(
|
||||
"username" => $target_user->username,
|
||||
"name" => $target_user->name,
|
||||
"level" => $target_user->level,
|
||||
"role" => cavern_level_to_role($target_user->level),
|
||||
"hash" => md5(strtolower($target_user->email)),
|
||||
"muted" => $target_user->muted,
|
||||
"posts_count" => $posts_count
|
||||
);
|
||||
|
||||
// user himself and admin can see user's email
|
||||
if ($user->username === $target_user->username || $user->level >= 8) {
|
||||
$data["email"] = $target_user->email;
|
||||
}
|
||||
|
||||
$data["login"] = $user->islogin;
|
||||
$data["fetch"] = round($_SERVER['REQUEST_TIME_FLOAT'] * 1000); // fit javascript timestamp
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
|
||||
function send_error($code, $message, $islogin) {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(array("status" => $message, "login" => $islogin, "fetch" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000))); // to fit javascript timestamp
|
||||
exit;
|
||||
}
|
14
config.php
Normal file
14
config.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
require_once('include/function.php');
|
||||
if(!session_id()) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
global $blog;
|
||||
|
||||
date_default_timezone_set("Asia/Taipei");
|
||||
|
||||
$blog['name'] = 'Cavern'; //網站名稱
|
||||
$blog['limit'] = 10; //首頁顯示文章數量
|
||||
$blog['register'] = true; //是否允許註冊
|
||||
?>
|
14
config.template
Normal file
14
config.template
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
require_once('include/function.php');
|
||||
if(!session_id()) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
global $blog;
|
||||
|
||||
date_default_timezone_set("Asia/Taipei");
|
||||
|
||||
$blog['name'] = '{blog_name}'; //網站名稱
|
||||
$blog['limit'] = {limit}; //首頁顯示文章數量
|
||||
$blog['register'] = {register}; //是否允許註冊
|
||||
?>
|
16
connection/SQL.php
Normal file
16
connection/SQL.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
if(!@$includepath){
|
||||
set_include_path('include/');
|
||||
}
|
||||
|
||||
error_reporting(E_ALL);
|
||||
require_once('db.php');
|
||||
|
||||
$database_SQL = ""; // 資料庫名稱
|
||||
$username_SQL = ""; // 連線帳號
|
||||
$password_SQL = ""; // 連線密碼
|
||||
$hostname_SQL = ""; // MySQL伺服器
|
||||
|
||||
global $SQL;
|
||||
$SQL = new Database($hostname_SQL,$username_SQL,$password_SQL,$database_SQL);
|
||||
$SQL->query("SET NAMES 'utf8mb4'");
|
80
include/article.php
Normal file
80
include/article.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
class NoPostException extends Exception {}
|
||||
|
||||
class Article {
|
||||
private $pid;
|
||||
private $author;
|
||||
private $name;
|
||||
private $title;
|
||||
private $content;
|
||||
private $time;
|
||||
private $likes_count;
|
||||
private $comments_count;
|
||||
private $islike = false;
|
||||
|
||||
public function __construct($data) {
|
||||
if (is_int($data)) {
|
||||
$this->pid = $data;
|
||||
$query = cavern_query_result("SELECT `post`.*, `user`.name FROM `post` INNER JOIN `user` ON `post`.username = `user`.username WHERE `pid`=%d", array($this->pid));
|
||||
if ($query['num_rows'] > 0) {
|
||||
$result = $query['row'];
|
||||
} else {
|
||||
// post doesn't exist
|
||||
throw new NoPostException('There is no post with pid '.$this->pid);
|
||||
}
|
||||
} else if (is_array($data)) {
|
||||
/* pass the sql result directly */
|
||||
$result = $data;
|
||||
$this->pid = $result['pid'];
|
||||
}
|
||||
|
||||
if (isset($result['name'])) $this->name = $result['name']; else $this->name = "";
|
||||
$this->author = $result['username'];
|
||||
$this->title = $result['title'];
|
||||
$this->content = $result['content'];
|
||||
$this->time = $result['time'];
|
||||
$this->likes_count = $result['like'];
|
||||
$this->comments_count = $result['comment'];
|
||||
}
|
||||
|
||||
public function __get($name) {
|
||||
return $this->$name;
|
||||
}
|
||||
|
||||
public function is_like(User $user) {
|
||||
if ($this->likes_count > 0 && $user->islogin) {
|
||||
$like_query = cavern_query_result("SELECT * FROM `like` WHERE `pid`='%d' AND `username`='%s'", array($this->pid, $_SESSION['cavern_username']));
|
||||
if ($like_query['num_rows'] > 0) {
|
||||
$this->islike = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->islike;
|
||||
}
|
||||
|
||||
public function modify(User $user, $name, $value) {
|
||||
// article author and admin can edit post
|
||||
if ($user->islogin && ($user->username === $this->author || $user->level >= 8)) {
|
||||
$this->$name = $value;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function save() {
|
||||
global $SQL;
|
||||
$SQL->query("UPDATE `post` SET `title`='%s', `content`='%s' WHERE `pid`='%d' AND `username`='%s'", array(htmlspecialchars($_POST['title']), htmlspecialchars($_POST['content']), $this->pid, $this->author));
|
||||
}
|
||||
}
|
||||
|
||||
function article_list($query_result) {
|
||||
$article_list = array();
|
||||
|
||||
if ($query_result['num_rows'] > 0) {
|
||||
do {
|
||||
$article_list[] = new Article($query_result['row']);
|
||||
} while ($query_result['row'] = $query_result['query']->fetch_assoc());
|
||||
}
|
||||
|
||||
return $article_list;
|
||||
}
|
440
include/css/cavern.css
Normal file
440
include/css/cavern.css
Normal file
@ -0,0 +1,440 @@
|
||||
/* General style */
|
||||
* {
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
div.table.wrapper {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.table.wrapper:not(:last-child) {
|
||||
margin-bottom: .75em;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
border: none;
|
||||
margin-top: 0 !important;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body h1:not(.ts):not(.unstyled) {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.markdown-body h2:not(.ts):not(.unstyled) {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-body h3:not(.ts):not(.unstyled) {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.markdown-body h4:not(.ts):not(.unstyled) {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.markdown-body h5:not(.ts):not(.unstyled) {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.markdown-body h6:not(.ts):not(.unstyled) {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.markdown-body > .markdown + * { /* .markdown is invisible and the first element in a post, so neighbor of .markdown is the first visible element */
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.markdown-body > *:last-child:not(div) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.editormd-html-preview {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.editormd-html-preview code { /* inline code */
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.editormd-html-preview a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.editormd-html-preview img {
|
||||
margin: .4em 0;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* fonts */
|
||||
/* prevent this rule from overwriting the style of <span> of KaTeX */
|
||||
.markdown-body *:not(span), .markdown-body > :not(.editormd-tex) span,
|
||||
/* editor font */
|
||||
[class*="CodeMirror"] *, [class*="cm"] * {
|
||||
font-family: Consolas,"SF Pro TC","SF Pro Text","SF Pro Icons","PingFang TC","Helvetica Neue","Helvetica","Microsoft JhengHei","Segoe UI",Ubuntu,微軟正黑體,"LiHei Pro","Droid Sans Fallback",Roboto,"Helvetica Neue","Droid Sans","Arial",sans-serif;
|
||||
}
|
||||
|
||||
.katex * { /* hack */
|
||||
font-family: KaTeX_Main, Times New Roman, serif;
|
||||
}
|
||||
|
||||
/* sweet alert */
|
||||
.swal2-popup h2.swal2-title {
|
||||
margin: 0 0 .4em;
|
||||
}
|
||||
|
||||
/* menu */
|
||||
#menu button.login.button {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
/* notification */
|
||||
#menu .notification.icon.item i.icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#menu .notification.icon.item span.counter {
|
||||
display: block;
|
||||
padding: .1em .2em;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
background-color: #F03434;
|
||||
border-radius: .2em;
|
||||
position: absolute;
|
||||
top: .25em;
|
||||
right: .25em;
|
||||
}
|
||||
|
||||
.notification.container {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 11; /* to overlap editormd */
|
||||
top: 1em;
|
||||
right: .2em;
|
||||
width: calc(100vw - 2.16em);
|
||||
max-width: 400px;
|
||||
height: 85vh;
|
||||
max-height: 500px;
|
||||
background-color: white;
|
||||
border-radius: .28571rem;
|
||||
box-shadow: 0 0 3px 0 #888888;
|
||||
}
|
||||
|
||||
.active.notification.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.notification.container > .ts.segment:first-child {
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
.notification.container .ts.feed {
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notification.container .ts.feed .event {
|
||||
padding-left: .8em;
|
||||
padding-right: .8em;
|
||||
}
|
||||
|
||||
.notification.container .ts.feed .unread.event {
|
||||
background-color: #e4f2f5;
|
||||
}
|
||||
|
||||
.notification.container .ts.feed .event:hover {
|
||||
background-color: #e2edef;
|
||||
}
|
||||
|
||||
.notification.click.handler {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.notification.container .ts.fluid.bottom:last-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ts.dividing.header .notification.description {
|
||||
font-size: .6em;
|
||||
color: gray;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
/* main */
|
||||
#main {
|
||||
padding: 10px 0 20px;
|
||||
}
|
||||
|
||||
/* content */
|
||||
#content {
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
/* pages */
|
||||
.loading#cards ~ #pages {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* cards */
|
||||
#cards {
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
.loading#cards {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ts.card > .content > .header:not(.ts) {
|
||||
font-size: 1.65em;
|
||||
}
|
||||
|
||||
.ts.card > .content > .description.markdown-body {
|
||||
background: transparent;
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ts.card > .content > .description.markdown-body a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* post */
|
||||
#content .ts.grid > #header > .ts.header {
|
||||
/* align this with post content */
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
#content .ts.grid > .action.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#content > .ts.segments:not(:last-child) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#post {
|
||||
font-size: 15px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.markdown-body ul:first-child, .markdown-body ol:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-body#post h1:not(.ts) {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.markdown-body#post h2:not(.ts) {
|
||||
font-size: 1.85em;
|
||||
}
|
||||
|
||||
.markdown-body#post h3:not(.ts) {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
.markdown-body#post h4:not(.ts) {
|
||||
font-size: 1.55em;
|
||||
}
|
||||
|
||||
.markdown-body#post h5:not(.ts) {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.markdown-body#post h6:not(.ts) {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
#toc {
|
||||
min-height: 8em;
|
||||
max-height: calc(95vh - 3em);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* code block */
|
||||
pre.prettyprint ol.linenums:not(.ts) {
|
||||
counter-reset: code 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
pre.prettyprint ol.linenums:not(.ts) > li::before {
|
||||
counter-increment: code;
|
||||
content: counter(code);
|
||||
|
||||
/* line numbers align */
|
||||
right: 100%;
|
||||
margin-left: 0;
|
||||
padding-right: .5em;
|
||||
}
|
||||
|
||||
pre.prettyprint ol.linenums:not(.ts) > li > code {
|
||||
min-height: 1em; /* fixing collapsed empty line */
|
||||
}
|
||||
|
||||
pre.prettyprint ol.linenums:not(.ts) > li > code > span {
|
||||
font-family: "YaHei Consolas Hybrid", 'Consolas', "Meiryo UI", "Malgun Gothic", "Segoe UI", "Trebuchet MS", Helvetica, monospace;
|
||||
}
|
||||
|
||||
/* post editor */
|
||||
#edit .action.column {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#markdownEditor:not(.editormd-fullscreen) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.editormd-fullscreen {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* comments */
|
||||
.ts.comments {
|
||||
min-height: 6em;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.ts.comments .comment {
|
||||
padding: 0.25em 0 0.25em;
|
||||
margin: 0.25em 0 0.25em;
|
||||
}
|
||||
|
||||
.ts.no-comment.segment:not(.active), .ts.active.loader + .fetch.button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment.header {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
padding-top: .8em;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.ts.comment.divider {
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
.stretched.header.column {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.comment .markdown-body {
|
||||
padding: .2em 0;
|
||||
}
|
||||
|
||||
.comment .markdown-body img {
|
||||
margin: .4em 0;
|
||||
}
|
||||
|
||||
.comment img.emoji {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.emphasized.comment {
|
||||
animation: commentEmphasize 2s ease-in .1s;
|
||||
}
|
||||
|
||||
/* comment editor */
|
||||
#comment > .ts.segment:first-child {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
#comment > .ts.segment:first-child > .ts.tabbed.menu {
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
#comment > .ts.segment:not(:first-child) {
|
||||
border-top: none;
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
border-bottom: 1px solid #e9e9e9;
|
||||
}
|
||||
|
||||
#comment .ts.button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#preview {
|
||||
min-height: 15em;
|
||||
}
|
||||
|
||||
/* account */
|
||||
.ts.label.avatar.tooltip {
|
||||
border: 0;
|
||||
border-radius: .21429rem;
|
||||
}
|
||||
|
||||
.ts.form .disabled.field {
|
||||
cursor: not-allowed;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.ts.form .disabled.field input {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* profile */
|
||||
#avatar {
|
||||
width: 7.5em;
|
||||
}
|
||||
|
||||
/* sidebar */
|
||||
#sidebar .ts.header .avatar.image {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#sidebar .ts.header .negative.sub.header {
|
||||
color: #CE5F58;
|
||||
}
|
||||
|
||||
/* footer */
|
||||
footer {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
footer .ts.divider {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@keyframes commentEmphasize {
|
||||
from {
|
||||
background-color: #e4f2f5;
|
||||
}
|
||||
|
||||
to {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
84
include/db.php
Normal file
84
include/db.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/* Cavern Edition
|
||||
modified by t510599 at 2019/05/30
|
||||
*/
|
||||
/*
|
||||
<Secret Blog>
|
||||
Copyright (C) 2012-2017 太陽部落格站長 Secret <http://gdsecret.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, version 3.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
class Database {
|
||||
private $conn;
|
||||
private $addr;
|
||||
private $user;
|
||||
private $pass;
|
||||
private $db;
|
||||
|
||||
public function __construct($addr,$user,$pass,$db){
|
||||
$this->addr = $addr;
|
||||
$this->user = $user;
|
||||
$this->pass = $pass;
|
||||
$this->db = $db;
|
||||
|
||||
$this->conn = new mysqli($addr,$user,$pass,$db);
|
||||
|
||||
if($this->conn->connect_error !== null){
|
||||
throw new Exception($this->conn->connect_error);
|
||||
}
|
||||
}
|
||||
|
||||
private function reconnect(){
|
||||
$this->conn = new mysqli($this->addr,$this->user,$this->pass,$this->db);
|
||||
}
|
||||
|
||||
private function checkConn(){
|
||||
return $this->conn->ping();
|
||||
}
|
||||
|
||||
public function query($query,$data = array()){
|
||||
if(!$this->checkConn()) $this->reconnect();
|
||||
|
||||
foreach($data as $k=>$d){
|
||||
$data[$k] = $this->conn->real_escape_string($d);
|
||||
}
|
||||
|
||||
$result = $this->conn->query(vsprintf($query,$data));
|
||||
|
||||
if($result === false){
|
||||
throw new Exception($this->conn->error);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function insert_id() {
|
||||
return $this->conn->insert_id;
|
||||
}
|
||||
};
|
117
include/function.php
Normal file
117
include/function.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
function cavern_login($username, $password) {
|
||||
global $SQL;
|
||||
if (isset($username) && isset($password)) {
|
||||
$login = $SQL->query("SELECT `username`, `pwd` FROM `user` WHERE `username` = '%s' AND `pwd` = '%s'",array($username, cavern_password_hash($password, $username)));
|
||||
if ($login->num_rows > 0) {
|
||||
$_SESSION['cavern_username'] = $username;
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function cavern_logout() {
|
||||
$_SESSION['cavern_username'] = NULL;
|
||||
unset($_SESSION['cavern_username']);
|
||||
return 1;
|
||||
}
|
||||
|
||||
function cavern_password_hash($value, $salt) {
|
||||
$temp = substr(sha1(strrev($value).$salt), 0, 24);
|
||||
return hash('sha512', $temp.$value);
|
||||
}
|
||||
|
||||
function cavern_query_result($query, $data=array()) {
|
||||
global $SQL;
|
||||
$result['query'] = $SQL->query($query, $data);
|
||||
$result['row'] = $result['query']->fetch_assoc();
|
||||
$result['num_rows'] = $result['query']->num_rows;
|
||||
if ($result['num_rows'] > 0) {
|
||||
return $result;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function cavern_level_to_role($level) {
|
||||
switch ($level) {
|
||||
case 9:
|
||||
$role = "站長";
|
||||
break;
|
||||
case 8:
|
||||
$role = "管理員";
|
||||
break;
|
||||
case 1:
|
||||
$role = "作者";
|
||||
break;
|
||||
case 0:
|
||||
$role = "會員";
|
||||
break;
|
||||
default:
|
||||
$role = "麥克雞塊";
|
||||
break;
|
||||
}
|
||||
return $role;
|
||||
}
|
||||
|
||||
function cavern_greeting() {
|
||||
$hour = date('G');
|
||||
if ($hour >= 21 || $hour < 5) {
|
||||
$greeting = "晚安";
|
||||
} else if ($hour >= 12) {
|
||||
$greeting = "午安";
|
||||
} else if ($hour >= 5 && $hour < 12) {
|
||||
$greeting = "早安";
|
||||
}
|
||||
return $greeting;
|
||||
}
|
||||
|
||||
function cavern_pages($now_page, $total, $limit) {
|
||||
$text='<div class="ts basic center aligned segment" id="pages">';
|
||||
$text.='<select class="ts basic dropdown" onchange="location.href=this.options[this.selectedIndex].value;">';
|
||||
$now_page = abs($now_page);
|
||||
$page_num = ceil($total / $limit);
|
||||
for ($i = 1; $i <= $page_num; $i++) {
|
||||
if ($now_page != $i) {
|
||||
$text.='<option value="index.php?page='.$i.'">第 '.$i.' 頁</option>';
|
||||
} else {
|
||||
$text.='<option value="index.php?page='.$i.'" selected="selected">第 '.$i.' 頁</option>';
|
||||
}
|
||||
}
|
||||
$text.='</select>';
|
||||
$text.='</div>';
|
||||
return $text;
|
||||
}
|
||||
|
||||
function sumarize($string, $limit) {
|
||||
$count = 0;
|
||||
$text = "";
|
||||
$content_start = FALSE;
|
||||
foreach (explode("\n", $string) as $line) {
|
||||
if (trim($line) != "" && $content_start == FALSE) {
|
||||
$content_start = TRUE; // don't count the empty line until the main content
|
||||
}
|
||||
if (!$content_start) {
|
||||
continue;
|
||||
}
|
||||
$count++;
|
||||
$text.=$line."\n";
|
||||
if ($count == $limit || mb_strlen($text) >= 200) {
|
||||
if (mb_strlen($text) >= 200) {
|
||||
$text = mb_substr($text, 0, 200)."...\n";
|
||||
}
|
||||
$text.="...(還有更多)\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $text;
|
||||
}
|
102
include/js/account.js
Normal file
102
include/js/account.js
Normal file
@ -0,0 +1,102 @@
|
||||
if (document.newacc) {
|
||||
// Register
|
||||
let form = document.newacc;
|
||||
eventListenerInitialize(form, [form.password, form.repeat]);
|
||||
$(form.username).on('change', function () {
|
||||
let self = this;
|
||||
if (!/^[a-z][a-z0-9_-]*$/.test(self.value) || (self.value.length > 20 || self.value == "")) {
|
||||
setFieldStatus(self, "error", "請依照格式輸入");
|
||||
setFieldLabel(self, "");
|
||||
} else {
|
||||
setFieldStatus(self, ""); // reset
|
||||
setFieldLabel(self, "");
|
||||
|
||||
axios.request({
|
||||
method: "GET",
|
||||
url: `ajax/user.php?username=${this.value}`,
|
||||
responseType: "json"
|
||||
}).then(function (_res) {
|
||||
// username exist
|
||||
setFieldStatus(self, "error", "此帳號已被使用");
|
||||
setFieldLabel(self, "此帳號已被使用");
|
||||
}).catch(function (_error) {
|
||||
// username not exist
|
||||
setFieldStatus(self, "success");
|
||||
setFieldLabel(self, "此帳號可以使用");
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (document.editacc) {
|
||||
// Manage Profile
|
||||
let form = document.editacc;
|
||||
eventListenerInitialize(form, [form.new, form.repeat]);
|
||||
$(form.new).on('input', function () {
|
||||
if (this.value == "") {
|
||||
form.repeat.removeAttribute("required");
|
||||
setFieldStatus(form.repeat, "", "", false);
|
||||
} else {
|
||||
form.repeat.setAttribute("required", "required");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function eventListenerInitialize (form, inputs) {
|
||||
// first is password input, second is repeat input
|
||||
inputs.forEach(function (el) {
|
||||
$(el).on('input', function (_e) {
|
||||
if (inputs[0].value == inputs[1].value && inputs[0].value != "") {
|
||||
setFieldStatus(inputs[1], "success");
|
||||
} else {
|
||||
setFieldStatus(inputs[1], "error", "密碼不正確,請再試一次。");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(form).on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
if (inputs[0].value != inputs[1].value) {
|
||||
inputs[1].setCustomValidity("密碼不正確,請再試一次。");
|
||||
inputs[1].focus();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let fd = new URLSearchParams(new FormData(this)).toString();
|
||||
axios.request({
|
||||
method: "POST",
|
||||
data: fd,
|
||||
url: "account.php",
|
||||
headers: {
|
||||
'Content-Type': "application/x-www-form-urlencoded"
|
||||
}
|
||||
}).then(function (res) {
|
||||
location.href = res.headers["axios-location"];
|
||||
}).catch(function (error) {
|
||||
if (error.response) {
|
||||
location.href = error.response.headers["axios-location"];
|
||||
} else {
|
||||
ts('.snackbar').snackbar({
|
||||
content: "發送失敗。"
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setFieldStatus(el, status, validity="", required=true) {
|
||||
el.parentElement.className = (required) ? `${status} required field` : `${status} field`;
|
||||
el.setCustomValidity(validity);
|
||||
}
|
||||
|
||||
function setFieldLabel(el, text) {
|
||||
let sibling = el.nextElementSibling;
|
||||
if (sibling.tagName == "SMALL" && text != "") {
|
||||
let span = document.createElement('span');
|
||||
span.className = "message";
|
||||
span.innerText = text;
|
||||
el.parentElement.insertBefore(span, sibling);
|
||||
} else if (sibling.tagName == "SPAN" && text == "") {
|
||||
$(sibling).remove();
|
||||
} else if (sibling.tagName != "SMALL") {
|
||||
sibling.innerText = text;
|
||||
}
|
||||
}
|
28
include/js/cards.js
Normal file
28
include/js/cards.js
Normal file
@ -0,0 +1,28 @@
|
||||
const cdnjs = "https://cdnjs.cloudflare.com/ajax/libs";
|
||||
|
||||
// Load Libraries
|
||||
const libraries = [
|
||||
cdnjs + "/marked/0.5.1/marked.min.js",
|
||||
cdnjs + "/prettify/r298/prettify.min.js",
|
||||
cdnjs + "/underscore.js/1.9.1/underscore-min.js"
|
||||
];
|
||||
|
||||
loadJS(libraries).then(function () {
|
||||
editormd.$marked = marked;
|
||||
editormd.loadFiles.js.push(...libraries.map(url => url.slice(0, -3))); // remove ".js"
|
||||
document.querySelectorAll('.ts.card .description').forEach(function(el) {
|
||||
let id = el.getAttribute('id');
|
||||
parseMarkdown(id, el.children[0].textContent, {
|
||||
toc: false,
|
||||
flowChart: false,
|
||||
sequenceDiagram: false,
|
||||
htmlDecode : "script,iframe,style|on*"
|
||||
}).children('.markdown').hide();
|
||||
});
|
||||
postProcess();
|
||||
setTimeout(function () {
|
||||
// show cards
|
||||
$('.loading#cards').removeClass('loading');
|
||||
$('#content .active.loader').removeClass('active');
|
||||
}, 500);
|
||||
});
|
328
include/js/comment.js
Normal file
328
include/js/comment.js
Normal file
@ -0,0 +1,328 @@
|
||||
const pid = $('#post').attr('data-id');
|
||||
|
||||
var post = { // post cache
|
||||
fetchTime: undefined,
|
||||
comments: [],
|
||||
idList: [] // ids of comment
|
||||
}
|
||||
|
||||
// Fetch
|
||||
$('.fetch.button').click(function(_e) {
|
||||
fetchComments();
|
||||
});
|
||||
|
||||
var fetchTimer = setInterval(fetchComments, 5*60*1000); // polling comments per 5 minutes
|
||||
|
||||
function fetchComments() {
|
||||
$('.ts.inline.loader').addClass('active');
|
||||
if (!pid) {
|
||||
console.error('An error occurred while fetching comments.');
|
||||
snackbar("無法載入留言。");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (post.fetchTime) document.cookie = `cavern_commentLastFetch=${post.fetchTime}; Max-Age=10`;
|
||||
|
||||
axios.request({
|
||||
method: "GET",
|
||||
url: `ajax/comment.php?pid=${pid}`,
|
||||
responseType: "json"
|
||||
}).then(function (res) {
|
||||
let data = res.data;
|
||||
let t = new Date(data.fetch);
|
||||
post.fetchTime = Math.ceil(data.fetch / 1000); // php timestamp
|
||||
$('span.fetch.time').text(`Last fetch: ${t.getHours()}:${ t.getMinutes() < 10 ? '0' + t.getMinutes() : t.getMinutes() }`);
|
||||
parseComments(data);
|
||||
}).catch(function (error) {
|
||||
if (error.response) {
|
||||
let res = error.response;
|
||||
console.error(`An error occurred while fetching comments of pid ${pid}, status ${res.status}`);
|
||||
} else {
|
||||
console.error(`An error occurred while fetching comments of pid ${pid}`);
|
||||
}
|
||||
snackbar("無法載入留言。");
|
||||
});
|
||||
setTimeout(() => { $('.ts.inline.loader').removeClass('active'); }, 250);
|
||||
}
|
||||
|
||||
function parseComments(data) {
|
||||
const commentTemplate = `<div class="comment" id="comment-{{ id }}" data-comment="{{ id }}"><a class="avatar" href="user.php?username={{ username }}"><img src="https://www.gravatar.com/avatar/{{ hash }}?d=https%3A%2F%2Ftocas-ui.com%2Fassets%2Fimg%2F5e5e3a6.png"></a><div class="content"><a class="author" href="user.php?username={{ username }}">{{ name }}</a><div class="middoted metadata"><div class="time">{{ time }}</div></div><div class="text" id="markdown-comment-{{ id }}"></div></div></div>`;
|
||||
|
||||
let add = data.idList.filter(function(item) { return post.idList.indexOf(item) < 0 }); // id list of new comments
|
||||
let remove = post.idList.filter(function(item) { return data.idList.indexOf(item) < 0 }); // id list of removed comments
|
||||
|
||||
for (postId of remove) {
|
||||
$(`.ts.comments div[data-comment="${postId}"]`).remove();
|
||||
}
|
||||
|
||||
for (c of data.comments) {
|
||||
if (add.indexOf(c.id) == -1 && data.modified.indexOf(c.id) == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (add.indexOf(c.id) != -1) {
|
||||
// render new comment
|
||||
let node = commentTemplate.replace(/{{ id }}/gm, c.id).replace('{{ time }}', c.time).replace(/{{ username }}/gm, c.username).replace('{{ name }}', data.names[c.username]).replace('{{ hash }}', data.hash[c.username]);
|
||||
$(node).appendTo('.ts.comments');
|
||||
if (c.actions.length != 0) {
|
||||
let actions = document.createElement('div');
|
||||
actions.className = "actions";
|
||||
$(`div[data-comment="${c.id}"] .content`).append(actions);
|
||||
for (act of c.actions) {
|
||||
switch (act) {
|
||||
case "reply":
|
||||
actions.insertAdjacentHTML('beforeend',`<a class="reply" data-username="${c.username}">回覆</a>`);
|
||||
break;
|
||||
case "del":
|
||||
actions.insertAdjacentHTML('beforeend',`<a class="delete" data-comment="${c.id}">刪除</a>`);
|
||||
break;
|
||||
case "edit":
|
||||
actions.insertAdjacentHTML('beforeend',`<a class="edit" data-comment="${c.id}">編輯</a>`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (data.modified.indexOf(c.id) != -1) {
|
||||
// empty the old content
|
||||
$(`#markdown-comment-${c.id}`).html('');
|
||||
}
|
||||
|
||||
if (c.modified) {
|
||||
let $metadata = $(`div[data-comment="${c.id}"] .metadata`);
|
||||
if ($metadata.children('.modified').length) {
|
||||
$metadata.children('.modified').attr('title', c.modified);
|
||||
} else {
|
||||
$metadata.append(`<div class="modified" title="${c.modified}">已編輯</div>`);
|
||||
}
|
||||
}
|
||||
|
||||
parseMarkdown(`markdown-comment-${c.id}`, _.unescape(c.markdown), {
|
||||
toc: false
|
||||
});
|
||||
}
|
||||
|
||||
post.comments = data.comments; // cache data
|
||||
post.idList = data.idList; // cache data
|
||||
postProcess();
|
||||
|
||||
/* jump to the comment and emphasize it */
|
||||
if (location.hash && location.hash.startsWith("#comment-")) {
|
||||
let commentID = location.hash;
|
||||
if (!$(commentID).length) {
|
||||
snackbar("留言已刪除或是不存在。")
|
||||
} else {
|
||||
$(window).scrollTop($(commentID).offset().top - $('.comment.header').outerHeight() - 10);
|
||||
$(commentID).addClass('emphasized');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.idList.length == 0) {
|
||||
$('.ts.no-comment.segment').addClass('active');
|
||||
} else {
|
||||
$('.ts.no-comment.segment').removeClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Comment Editor & Preview
|
||||
(function () {
|
||||
let commentContainer = document.querySelector('#comment');
|
||||
let textarea = commentContainer.querySelector('textarea');
|
||||
$('.ts.tabbed.menu a.item[data-tab="preview"]').click(function() {
|
||||
let comment = textarea.value;
|
||||
if (comment.trim() != '') {
|
||||
// reset the container
|
||||
$('#preview').html('');
|
||||
parseMarkdown('preview', comment, {
|
||||
toc: false
|
||||
});
|
||||
postProcess();
|
||||
} else {
|
||||
$('#preview').html('Nothing to preview!');
|
||||
}
|
||||
});
|
||||
|
||||
$('#comment textarea').keydown(function (e) {
|
||||
if (e.ctrlKey && (e.keyCode == 10 || e.keyCode == 13)) { // Ctrl-Enter pressed; Chrome: keyCode == 10
|
||||
document.querySelector('#comment div[data-tab="textarea"] button.submit.positive').click(); // send comment
|
||||
}
|
||||
});
|
||||
|
||||
// Edit
|
||||
$('.ts.comments').on('click', '.edit', function(e) {
|
||||
if (!textarea.disabled) {
|
||||
let el = e.currentTarget;
|
||||
let id = el.dataset.comment;
|
||||
editorEditComment(textarea, id);
|
||||
} else {
|
||||
snackbar("你已被禁言。");
|
||||
}
|
||||
});
|
||||
|
||||
// Reply
|
||||
$('.ts.comments').on('click', '.reply', function(e) {
|
||||
if (!textarea.disabled) {
|
||||
let el = e.currentTarget;
|
||||
textarea.value += ` @${el.dataset.username} `;
|
||||
textarea.focus();
|
||||
} else {
|
||||
snackbar("你已被禁言。");
|
||||
}
|
||||
});
|
||||
|
||||
function editorInitialize(edtior) {
|
||||
delete commentContainer.dataset.editId;
|
||||
if ($('#comment .action.buttons button.cancel').length) {
|
||||
$('#comment .action.buttons button.cancel').remove();
|
||||
}
|
||||
if ($('#comment .menu .indicator').length) {
|
||||
$('#comment .menu .indicator').remove();
|
||||
}
|
||||
edtior.value = ""; // empty the textarea
|
||||
}
|
||||
|
||||
function editorEditComment(editor, commentId) {
|
||||
if (post.idList.indexOf(commentId) == -1) {
|
||||
snackbar('留言已刪除。');
|
||||
return undefined;
|
||||
}
|
||||
commentContainer.dataset.editId = commentId;
|
||||
if (!$('#comment .action.buttons button.cancel').length) {
|
||||
let cancelButton = document.createElement('button');
|
||||
cancelButton.classList.add('ts', 'cancel', 'button');
|
||||
cancelButton.innerText = "取消";
|
||||
commentContainer.querySelector('.action.buttons').appendChild(cancelButton);
|
||||
cancelButton.addEventListener('click', function () {
|
||||
editorInitialize(editor);
|
||||
});
|
||||
}
|
||||
if (!$('#comment .menu .indicator').length) {
|
||||
let indicator = document.createElement('div');
|
||||
indicator.classList.add('right', 'indicator', 'item');
|
||||
indicator.innerText = `Editing: ${commentId}`;
|
||||
commentContainer.querySelector('.menu').appendChild(indicator);
|
||||
} else {
|
||||
$('#comment .menu .indicator').text(`Editing: ${commentId}`);
|
||||
}
|
||||
editor.value = post.comments[post.idList.indexOf(commentId)].markdown;
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
// Send Comment
|
||||
let commentLock = false;
|
||||
const commentRate = 10; // 1 comment per 10 seconds
|
||||
|
||||
$('#comment div[data-tab="textarea"] button.submit.positive').click(function() {
|
||||
var _this = this;
|
||||
let content = textarea.value;
|
||||
if (content.trim() == "") {
|
||||
snackbar("留言不能為空!");
|
||||
return false;
|
||||
}
|
||||
if (commentLock) {
|
||||
snackbar(`每 ${commentRate} 秒只能發一則留言。`);
|
||||
return false;
|
||||
} else if (!commentContainer.dataset.editId) {
|
||||
// only new comment should be limited
|
||||
commentLock = true;
|
||||
}
|
||||
|
||||
if (commentContainer.dataset.editId) {
|
||||
// edit comment
|
||||
var commentData = new URLSearchParams({
|
||||
"edit": commentContainer.dataset.editId,
|
||||
"content": content
|
||||
}).toString();
|
||||
} else {
|
||||
// new comment
|
||||
var commentData = new URLSearchParams({
|
||||
"pid": pid,
|
||||
"content": content
|
||||
}).toString();
|
||||
}
|
||||
|
||||
axios.request({
|
||||
method: "POST",
|
||||
url: "ajax/comment.php",
|
||||
data: commentData,
|
||||
responseType: "json"
|
||||
}).then(function (res) {
|
||||
editorInitialize(textarea);
|
||||
console.log(`Comment sent succeessfully! Comment id is ${res.data["comment_id"]}`);
|
||||
setTimeout(function() { commentLock = false }, commentRate * 1000); // sec -> microsecond
|
||||
fetchComments();
|
||||
}).catch(function (error) {
|
||||
commentLock = false; // unlock the textarea
|
||||
if (error.response) {
|
||||
let res = error.response;
|
||||
let data = res.data;
|
||||
console.error(`An error occurred while sending comments of pid ${pid}, status ${res.status}`);
|
||||
switch (data.status) {
|
||||
case "empty":
|
||||
snackbar("留言不能為空!");
|
||||
break;
|
||||
case "ratelimit":
|
||||
let remainSeconds = res.headers['retry-after'];
|
||||
snackbar(`每 ${commentRate} 秒只能發一則留言。請 ${remainSeconds} 秒後再試!`);
|
||||
break;
|
||||
case "muted":
|
||||
snackbar("你已被禁言。");
|
||||
$('#comment .ts.fluid.input').addClass('disabled');
|
||||
$(textarea).attr("placeholder", "你被禁言了。").val(""); // empty the textarea
|
||||
$(_this).addClass('disabled').text("你被禁言了");
|
||||
break;
|
||||
case "author":
|
||||
snackbar("你不能編輯別人的留言!");
|
||||
break;
|
||||
case "nologin":
|
||||
snackbar("請先登入。");
|
||||
break;
|
||||
default:
|
||||
snackbar("發送失敗。");
|
||||
break;
|
||||
}
|
||||
fetchComments();
|
||||
} else {
|
||||
console.error(`An error occurred while sending comments of pid ${pid}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// Delete
|
||||
$('.ts.comments').on('click', '.delete', function(e) {
|
||||
let el = e.currentTarget;
|
||||
let id = el.dataset.comment;
|
||||
swal({
|
||||
type: 'question',
|
||||
title: '確定要刪除嗎?',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '確定',
|
||||
cancelButtonText: '取消',
|
||||
}).then((result) => {
|
||||
if (result.value) { // confirm
|
||||
axios.request({
|
||||
method: "GET",
|
||||
url: "ajax/comment.php?del=" + id,
|
||||
responseType: "json"
|
||||
}).then(function (_res) {
|
||||
fetchComments();
|
||||
}).catch(function (error) {
|
||||
if (error.response) {
|
||||
let res = error.response;
|
||||
console.error(`An error occurred while deleting comment of id ${id}, status ${res.status}`);
|
||||
} else {
|
||||
console.error(`An error occurred while deleting comment of id ${id}`);
|
||||
}
|
||||
snackbar('刪除失敗。');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function snackbar(message) {
|
||||
ts('.snackbar').snackbar({
|
||||
content: message
|
||||
});
|
||||
}
|
176
include/js/edit.js
Normal file
176
include/js/edit.js
Normal file
@ -0,0 +1,176 @@
|
||||
let edtior;
|
||||
editormd.urls = {
|
||||
atLinkBase : "user.php?username="
|
||||
};
|
||||
|
||||
// create editor instance
|
||||
editor = editormd('markdownEditor', {
|
||||
height: 450,
|
||||
path: "https://pandao.github.io/editor.md/lib/",
|
||||
markdown: document.edit.content.value,
|
||||
htmlDecode : "script,iframe|on*",
|
||||
placeholder: '',
|
||||
styleActiveLine: false,
|
||||
"font-size": '14px',
|
||||
emoji: true,
|
||||
taskList: true,
|
||||
tex: true,
|
||||
flowChart: true,
|
||||
sequenceDiagram: true,
|
||||
watch: false,
|
||||
lineNumbers: false,
|
||||
lineWrapping: false,
|
||||
toolbarAutoFixed: false,
|
||||
toolbarIcons : function() {
|
||||
return [
|
||||
"search", "|",
|
||||
"undo", "redo", "|",
|
||||
"bold", "del", "italic", "|",
|
||||
"list-ul", "list-ol", "emoji", "html-entities", "|",
|
||||
"link", "image", "|",
|
||||
"preview", "fullscreen", "||",
|
||||
"help", "info",
|
||||
]
|
||||
},
|
||||
toolbarIconsClass: {
|
||||
preview: 'fa-eye'
|
||||
},
|
||||
onload: function() {
|
||||
var __this__ = this;
|
||||
$('ul.editormd-menu').addClass('unstyled'); // remove style of TocasUI
|
||||
$('ul.editormd-menu i[name="emoji"]').parent().click(function () { // remove style of TocasUI from emoji tab (hack)
|
||||
setTimeout(()=>{ $('ul.editormd-tab-head').addClass('unstyled'); }, 300);
|
||||
});
|
||||
this.resize();
|
||||
loadDraft();
|
||||
|
||||
document.edit.title.addEventListener("keydown", function () {
|
||||
saveDraft(__this__);
|
||||
})
|
||||
|
||||
this.cm.on("change", function(_cm, _changeObj) {
|
||||
saveDraft(__this__);
|
||||
});
|
||||
},
|
||||
onresize: function() {
|
||||
if (this.state.preview) {
|
||||
requestAnimationFrame(()=>{
|
||||
this.previewed();
|
||||
this.previewing();
|
||||
});
|
||||
}
|
||||
},
|
||||
onpreviewing: function() {
|
||||
this.save();
|
||||
// use tocas-ui style tables
|
||||
$('table').each((_i,e) => {
|
||||
$(e).addClass('ts celled table').css('display','table');
|
||||
});
|
||||
|
||||
// prevent user from destroying page style
|
||||
var parser = new cssjs();
|
||||
let stylesheets = document.querySelectorAll('.markdown-body style');
|
||||
for (let style of stylesheets) {
|
||||
let ruleSource = style.innerHTML;
|
||||
let cssObject = parser.parseCSS(ruleSource);
|
||||
for (let rule of cssObject) {
|
||||
let valid = false;
|
||||
let validPrefix = [".markdown-body ", ".editormd-preview-container ", ".markdown-body.editormd-preview-container ", ".editormd-preview-container.markdown-body "];
|
||||
validPrefix.forEach((e, _i) => {
|
||||
valid = valid || rule.selector.startsWith(e);
|
||||
});
|
||||
|
||||
if (!rule.selector.startsWith('@')) { // '@keyframe' & '@import'
|
||||
if (!valid) {
|
||||
rule.selector = ".editormd-preview-container " + rule.selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
style.innerHTML = parser.getCSSForEditor(cssObject);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// save draft data
|
||||
function saveDraft(editor) {
|
||||
localStorage.setItem('cavern_draft_title', document.edit.title.value);
|
||||
localStorage.setItem('cavern_draft_id', document.edit.pid.value);
|
||||
localStorage.setItem('cavern_draft_content', editor.getMarkdown());
|
||||
localStorage.setItem('cavern_draft_time', new Date().getTime());
|
||||
}
|
||||
|
||||
// Ask if user want to load draft
|
||||
function loadDraft() {
|
||||
if ($('#pid').val() == localStorage.getItem('cavern_draft_id')) {
|
||||
swal({
|
||||
type: 'question',
|
||||
title: '要載入上次備份嗎?',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '載入',
|
||||
cancelButtonText: '取消',
|
||||
}).then((result) => {
|
||||
if (result.value) { // confirm
|
||||
document.edit.title.value = localStorage.getItem('cavern_draft_title');
|
||||
editor.setValue(localStorage.getItem('cavern_draft_content'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Post an article
|
||||
$(document.edit).on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var _this = this;
|
||||
let fd = new URLSearchParams(new FormData(this)).toString();
|
||||
axios.request({
|
||||
method: "POST",
|
||||
data: fd,
|
||||
url: "post.php",
|
||||
headers: {
|
||||
'Content-Type': "application/x-www-form-urlencoded"
|
||||
}
|
||||
}).then(function (res) {
|
||||
if (_this.pid.value == localStorage.getItem('cavern_draft_id')) {
|
||||
['id', 'title', 'content', 'time'].forEach((name) => {
|
||||
localStorage.removeItem(`cavern_draft_${name}`);
|
||||
});
|
||||
}
|
||||
location.href = res.headers["axios-location"];
|
||||
}).catch(function (error) {
|
||||
if (error.response) {
|
||||
location.href = error.response.headers["axios-location"];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete post confirm message
|
||||
$('.action.column .delete').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var el = this;
|
||||
var href = el.getAttribute('href');
|
||||
swal({
|
||||
type: 'question',
|
||||
title: '確定要刪除嗎?',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '確定',
|
||||
cancelButtonText: '取消',
|
||||
}).then((result) => {
|
||||
if (result.value) { // confirm
|
||||
axios.request({
|
||||
method: "GET",
|
||||
url: href
|
||||
}).then(function (res) {
|
||||
location.href = res.headers["axios-location"];
|
||||
}).catch(function (error){
|
||||
if (error.response) {
|
||||
location.href = error.response.headers["axios-location"];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// session keep alive
|
||||
setInterval(function() {
|
||||
$.get('ajax/posts.php');
|
||||
}, 5*60*1000);
|
3
include/js/lib/css.min.js
vendored
Normal file
3
include/js/lib/css.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4604
include/js/lib/editormd.js
Normal file
4604
include/js/lib/editormd.js
Normal file
File diff suppressed because it is too large
Load Diff
74
include/js/lib/tocas.js
Normal file
74
include/js/lib/tocas.js
Normal file
@ -0,0 +1,74 @@
|
||||
var Tocas,animationEnd,bindModalButtons,closeModal,contractDropdown,detectDropdown,expandDropdown,quadrant,slider_progressColor,slider_trackColor,z_dropdownActive,z_dropdownHovered,z_dropdownMenu;Tocas=(function(){var compact,dropzoneNumber,emptyArray,filter,isArray,isEmptyOrWhiteSpace,isObject,queue,slice,tocas,ts;ts=void 0;emptyArray=[];slice=emptyArray.slice;filter=emptyArray.filter;queue=[];tocas={};isArray=Array.isArray||function(obj){return obj instanceof Array;};isObject=function(obj){return obj instanceof Object;};isEmptyOrWhiteSpace=function(str){return str===null||str.match(/^\s*$/)!==null;};dropzoneNumber=0;compact=function(array){return filter.call(array,function(item){return item!==null;});};tocas.init=function(selector,context){var dom;dom=void 0;if(typeof selector==='string'){if(selector[0]==='<'){return tocas.fragment(selector);}
|
||||
selector=selector.trim();if(typeof context!=='undefined'){return ts(selector).find(context);}
|
||||
dom=tocas.select(document,selector);}else if(tocas.isTocas(selector)){return selector;}else{if(isArray(selector)){dom=compact(selector);}else if(isObject(selector)){dom=[selector];selector=null;}}
|
||||
return tocas.Tocas(dom,selector);};tocas.fragment=function(selector){var $element,attrObj,attrs,content,contentMatch,contentRegEx,hasAttr,hasContent,i,mainAll,mainAttrs,mainElement,match,noContent,regEx;noContent=/^<([^\/].*?)>$/;regEx=/(?:<)(.*?)( .*?)?(?:>)/;match=regEx.exec(selector);mainAll=match[0];mainElement=match[1];mainAttrs=match[2];hasAttr=typeof mainAttrs!=='undefined';hasContent=!mainAll.match(noContent);if(hasContent){contentRegEx=new RegExp(mainAll+'(.*?)(?:</'+mainElement+'>)$');contentMatch=contentRegEx.exec(selector);content=contentMatch[1];}
|
||||
if(hasAttr){attrs=mainAttrs.split(/(?:\s)?(.*?)=(?:"|')(.*?)(?:"|')/).filter(Boolean);attrObj={};i=0;while(i<attrs.length){if((i+2)%2===0){attrObj[attrs[i]]=attrs[i+1];}
|
||||
i++;}}
|
||||
$element=ts(document.createElement(mainElement));if(hasAttr){$element.attr(attrObj);}
|
||||
if(hasContent){$element.html(content);}
|
||||
return $element;};tocas.isTocas=function(obj){return obj instanceof tocas.Tocas;};tocas.select=function(element,selector){var e;try{return slice.call(element.querySelectorAll(selector));}catch(error){e=error;console.log('TOCAS ERROR: Something wrong while selecting '+selector+' element.');}};tocas.Tocas=function(dom,selector){dom=dom||[];dom.__proto__=ts.fn;dom.selector=selector||'';return dom;};ts=function(selector,context){if(typeof selector==='function'){document.addEventListener('DOMContentLoaded',selector);}else{return tocas.init(selector,context);}};ts.fn={each:function(callback){emptyArray.every.call(this,function(index,element){return callback.call(index,element,index)!==false;});return this;},slice:function(){return ts(slice.apply(this,arguments));},eq:function(index){return this.slice(index,index+1);}};if(!window.ts){window.ts=ts;}})(Tocas);ts.fn.on=function(eventName,selector,handler,once){var hasSelector;once=once||false;hasSelector=true;if(typeof selector!=='string'){hasSelector=false;handler=selector;}
|
||||
if(typeof handler!=='function'){once=handler;}
|
||||
return this.each(function(){var data,event,eventHandler,events,i;if(typeof this.addEventListener==='undefined'){console.log('TOCAS ERROR: Event listener is not worked with this element.');return false;}
|
||||
if(typeof this.ts_eventHandler==='undefined'){this.ts_eventHandler={};}
|
||||
events=eventName.split(' ');for(i in events){event=events[i];if(typeof this.ts_eventHandler[event]==='undefined'){this.ts_eventHandler[event]={registered:false,list:[]};}
|
||||
if(this.ts_eventHandler[event].registered===false){this.addEventListener(event,function(evt){var e,inSelector;if(typeof this.ts_eventHandler[event]!=='undefined'){for(e in this.ts_eventHandler[event].list){if(typeof this.ts_eventHandler[event].list[e].selector!=='undefined'){inSelector=false;ts(this.ts_eventHandler[event].list[e].selector).each(function(i,el){if(evt.target===el){inSelector=true;}});if(!inSelector){return;}}
|
||||
this.ts_eventHandler[event].list[e].func.call(this,evt);if(this.ts_eventHandler[event].list[e].once){delete this.ts_eventHandler[event].list[e];}}}});this.ts_eventHandler[event].registered=true;}
|
||||
eventHandler=this.ts_eventHandler[event].list;data={func:handler,once:once};if(hasSelector){data.selector=selector;}
|
||||
eventHandler.push(data);this.ts_eventHandler[event].list=eventHandler;}});};ts.fn.one=function(eventName,selector,handler){return this.each(function(){ts(this).on(eventName,selector,handler,true);});};ts.fn.off=function(eventName,handler){return this.each(function(){var e;if(typeof this.ts_eventHandler==='undefined'){return;}
|
||||
if(typeof this.ts_eventHandler[eventName]==='undefined'){return;}
|
||||
console.log(handler);if(typeof handler==='undefined'){this.ts_eventHandler[eventName].list=[];return;}
|
||||
for(e in this.ts_eventHandler[eventName].list){if(handler===this.ts_eventHandler[eventName].list[e].func){delete this.ts_eventHandler[eventName].list[e];}}});};ts.fn.css=function(property,value){var css,cssObject,i;css='';if(property!==null&&value!==null){css=property+':'+value+';';}else if(typeof property==='object'&&!Array.isArray(property)&&value===null){for(i in property){if(property.hasOwnProperty(i)){css+=i+':'+property[i]+';';}}}else if(Array.isArray(property)&&value===null){cssObject={};this.each(function(){var i;for(i in property){cssObject[property[i]]=ts(this).getCss(property[i]);}});return cssObject;}else if(property!==null&&value===null){return ts(this).getCss(property);}
|
||||
return this.each(function(){if(typeof this.style==='undefined'){return;}
|
||||
this.style.cssText=this.style.cssText+css;});};ts.fn.hasClass=function(classes){if(0 in this){if(this[0].classList){return this[0].classList.contains(classes);}else{return new RegExp('(^| )'+classes+'( |$)','gi').test(this[0].className);}}};ts.fn.classList=function(){var i;var classes,i;classes=[];if(0 in this){if(this[0].classList){i=0;while(i<this[0].classList.length){classes.push(this[0].classList[i]);i++;}}else{for(i in this[0].className.split(' ')){classes.push(this[0].className.split(' ')[i]);}}}
|
||||
return classes;};ts.fn.addClass=function(classes){if(classes===null){return;}
|
||||
return this.each(function(){var i,list;list=classes.split(' ');for(i in list){if(list[i]===''){i++;continue;}
|
||||
if(this.classList){this.classList.add(list[i]);}else{this.className+=' '+list[i];}}});};ts.fn.removeClass=function(classes){return this.each(function(){var i,list;if(!classes){this.className='';}else{list=classes.split(' ');for(i in list){if(list[i]===''){i++;continue;}
|
||||
if(this.classList){this.classList.remove(list[i]);}else if(typeof this.className!=='undefined'){this.className=this.className.replace(new RegExp('(^|\\b)'+classes.split(' ').join('|')+'(\\b|$)','gi'),' ');}}}});};ts.fn.toggleClass=function(classes){return this.each(function(){var i,index,list,objClassList;list=void 0;index=void 0;objClassList=void 0;list=classes.split(' ');for(i in list){if(this.classList){this.classList.toggle(list[i]);}else{objClassList=this.className.split(' ');index=list.indexOf(list[i]);if(index>=0){objClassList.splice(index,1);}else{objClassList.push(list[i]);}
|
||||
this.className=list[i].join(' ');}}});};ts.fn.getCss=function(property){var err;try{if(0 in this){return document.defaultView.getComputedStyle(this[0],null).getPropertyValue(property);}else{return null;}}catch(error){err=error;return null;}};ts.fn.remove=function(){return this.each(function(){this.parentNode.removeChild(this);});};ts.fn.children=function(){var list;list=[];this.each(function(i,el){list.push.apply(list,el.children);});return ts(list);};ts.fn.find=function(selector){var list;if(typeof selector!=='string'){return null;}
|
||||
list=[];this.each(function(i,el){list.push.apply(list,el.querySelectorAll(selector));});if(list.length){return ts(list);}else{return null;}};ts.fn.parent=function(){if(0 in this){return ts(this[0].parentNode);}else{return null;}};ts.fn.parents=function(selector){var selector;var selector;var parents,that;that=this;selector=selector||null;parents=[];if(selector!==null){selector=ts(selector);}
|
||||
while(that){that=ts(that).parent()[0];if(!that){break;}
|
||||
if(selector===null||selector!==null&&Array.prototype.indexOf.call(selector,that)!==-1){parents.push(that);}}
|
||||
return ts(parents);};ts.fn.closest=function(selector){var selector;var that;that=this;selector=ts(selector);while(true){that=ts(that).parent()[0];if(!that){return null;}
|
||||
if(Array.prototype.indexOf.call(selector,that)!==-1){return ts(that);}}};ts.fn.contains=function(wants){var isTrue,selector;selector=ts(wants);isTrue=false;this.each(function(i,el){var children,si;children=el.childNodes;si=0;while(si<selector.length){if(Array.prototype.indexOf.call(children,selector[si])!==-1){isTrue=true;}
|
||||
si++;}});return isTrue;};ts.fn.attr=function(attr,value){value=value===null?null:value;if(typeof attr==='object'&&!value){return this.each(function(){var i;for(i in attr){this.setAttribute(i,attr[i]);}});}else if(attr!==null&&typeof value!=='undefined'){return this.each(function(){this.setAttribute(attr,value);});}else if(attr!==null&&!value){if(0 in this){return this[0].getAttribute(attr);}else{return null;}}};ts.fn.removeAttr=function(attr){return this.each(function(){this.removeAttribute(attr);});};animationEnd='webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend';quadrant=function(el){var height,heightHalf,position,width,widthHalf;position=el.getBoundingClientRect();width=window.innerWidth;widthHalf=width / 2;height=window.innerHeight;heightHalf=height / 2;if(position.left<widthHalf&&position.top<heightHalf){return 2;}else if(position.left<widthHalf&&position.top>heightHalf){return 3;}else if(position.left>widthHalf&&position.top>heightHalf){return 4;}else if(position.left>widthHalf&&position.top<heightHalf){return 1;}};z_dropdownMenu=9;z_dropdownActive=10;z_dropdownHovered=11;slider_trackColor="#e9e9e9";slider_progressColor="rgb(150, 150, 150)";expandDropdown=function(target){return ts(target).css('z-index',z_dropdownActive).removeClass('hidden').addClass('visible').addClass('animating').one(animationEnd,function(){return ts(target).removeClass('animating');});};contractDropdown=function(target){return ts(target).css('z-index',z_dropdownMenu).removeClass('visible').addClass('hidden').addClass('animating').one(animationEnd,function(){return ts(target).removeClass('animating');});};detectDropdown=function(target,event){var hasDropdownParent,isDropdown,isDropdownIcon,isDropdownImage,isDropdownText,isItem,isTsMenuItem,parentIsItem,targetIsDropdown;isDropdown=ts(target).hasClass('dropdown');isDropdownText=ts(event.target).hasClass('text');isDropdownIcon=ts(event.target).hasClass('icon');isDropdownImage=ts(event.target).hasClass('image');hasDropdownParent=ts(event.target).parent().hasClass('dropdown');parentIsItem=ts(event.target).parent().hasClass('item');targetIsDropdown=ts(event.target).hasClass('dropdown');isItem=ts(event.target).hasClass('item');isTsMenuItem=ts(event.target).closest('.ts.menu');if((isTsMenuItem&&isDropdown&&parentIsItem&&targetIsDropdown)||(isTsMenuItem&&isDropdown&&!parentIsItem&&targetIsDropdown)||(isTsMenuItem&&isDropdown&&hasDropdownParent&&parentIsItem)){return expandDropdown(target);}else if((isDropdown&&isItem)||(isDropdown&&parentIsItem)){return contractDropdown('.ts.dropdown.visible');}else if(isDropdown&&isTsMenuItem){return expandDropdown(target);}else if(isDropdown&&targetIsDropdown){return expandDropdown(target);}else if(isDropdown&&isDropdownIcon&&hasDropdownParent){return expandDropdown(target);}else if(isDropdown&&isDropdownImage&&hasDropdownParent){return expandDropdown(target);}else if(isDropdown&&isDropdownText&&hasDropdownParent){return expandDropdown(target);}};ts(document).on('click',function(event){if(ts(event.target).closest('.dropdown:not(.basic)')===null&&!ts(event.target).hasClass('dropdown')){return contractDropdown('.ts.dropdown:not(.basic).visible');}});ts.fn.dropdown=function(command){return this.each(function(){return ts(this).on('click',function(e){ts(this).removeClass('upward downward leftward rightward');if(quadrant(this)===2){ts(this).addClass('downward rightward');}else if(quadrant(this)===3){ts(this).addClass('upward rightward');}else if(quadrant(this)===1){ts(this).addClass('downward leftward');}else if(quadrant(this)===4){ts(this).addClass('upward leftward');}
|
||||
contractDropdown('.ts.dropdown.visible');return detectDropdown(this,e);});});};ts.fn.checkbox=function(){return this.each(function(){return ts(this).on('click',function(e){var isRadio,name,tsThis;isRadio=ts(this).hasClass('radio');if(isRadio){tsThis=ts(this).find('input[type="radio"]');}else{tsThis=ts(this).find('input[type="checkbox"]');}
|
||||
if(tsThis===null){}else if(isRadio){name=tsThis.attr('name');ts(`input[type='radio'][name='${name}']`).removeAttr('checked');return tsThis.attr('checked','checked');}else{if(tsThis.attr('checked')==='checked'){return tsThis.removeAttr('checked');}else{return tsThis.attr('checked','checked');}}});});};ts.fn.tablesort=function(){return this.each(function(){var table;if(!ts(this).hasClass("sortable")){return;}
|
||||
table=this;return ts(this).find("thead th").each(function(i){return ts(this).on("click",function(){var isAsc,sortTable;isAsc=ts(this).hasClass('ascending');ts(this).closest('thead').find('th').removeClass('sorted ascending descending');sortTable=function(table,col,reverse){var element,j,len,results,tb,tr;tb=table.tBodies[0];tr=Array.prototype.slice.call(tb.rows,0);reverse=-((+reverse)||-1);tr=tr.sort(function(a,b){return reverse*(a.cells[col].textContent.trim().localeCompare(b.cells[col].textContent.trim()));});results=[];for(j=0,len=tr.length;j<len;j++){element=tr[j];results.push(tb.appendChild(element));}
|
||||
return results;};sortTable(table,i,isAsc);return ts(this).addClass(isAsc?'sorted descending':'sorted ascending');});});});};closeModal=function(modal){if(ts(modal).hasClass('opening')||ts(modal).hasClass('closing')){return;}
|
||||
ts(modal).closest('.ts.modals.dimmer').addClass('closing').one(animationEnd,function(){var dimmer;dimmer=this;return setTimeout(function(){ts(dimmer).removeClass('closing').removeClass('active');return ts('body').removeAttr('data-modal-lock');},30);});return ts(modal).addClass('closing').one(animationEnd,function(){return ts(this).removeClass('closing').removeAttr('open');});};bindModalButtons=function(modal,approve,deny,approveCallback,denyCalback,overwrite){var isset,tsApprove,tsDeny;tsApprove=ts(modal).find(approve);tsDeny=ts(modal).find(deny);isset=ts(modal).attr("data-modal-initialized")!==null;if(tsApprove!==null){if(overwrite){tsApprove.off('click');}
|
||||
if(overwrite||!isset&&!overwrite){tsApprove.on('click',function(){if(approveCallback.call(modal)!==false){return closeModal(modal);}});}}
|
||||
if(tsDeny!==null){if(overwrite){tsDeny.off('click');}
|
||||
if(overwrite||!isset&&!overwrite){tsDeny.on('click',function(){if(denyCalback.call(modal)!==false){return closeModal(modal);}});}}
|
||||
return ts(modal).attr('data-modal-initialized','true');};ts.fn.modal=function(option){return this.each(function(i){var approve,closeBtn,deny,modal,onApprove,onDeny,tsDimmer,tsModal;if(i>0||typeof this==='undefined'){return;}
|
||||
modal=this;tsModal=ts(this);tsDimmer=tsModal.closest('.ts.modals.dimmer');closeBtn=tsModal.find('.close.icon');if(tsDimmer===null){return;}
|
||||
if(option==='show'){ts('body').attr('data-modal-lock','true');tsDimmer.addClass('active').addClass('opening').one(animationEnd,function(){return ts(this).removeClass('opening');}).on('click',function(e){if(ts(modal).hasClass('closable')){if(e.target===this){return closeModal(modal);}}});if(closeBtn!==null){closeBtn.on('click',function(){return closeModal(modal);});}
|
||||
bindModalButtons(modal,'.positive, .approve, .ok','.negative, .deny, .cancel',function(){return true;},function(){return true;},false);return tsModal.attr('open','open').addClass('opening').one(animationEnd,function(){return tsModal.removeClass('opening');});}else if(option==='hide'){return closeModal(this);}else if(typeof option==='object'){approve=option.approve||'.positive, .approve, .ok';deny=option.deny||'.negative, .deny, .cancel';onDeny=option.onDeny||function(){return true;};onApprove=option.onApprove||function(){return true;};modal=this;return bindModalButtons(modal,approve,deny,onApprove,onDeny,true);}});};ts.fn.sidebar=function(options,selector,eventName){var closable,closeVisibleSidebars,dimPage,exclusive,pusher,scrollLock;dimPage=(options!=null?options.dimPage:void 0)||false;exclusive=(options!=null?options.exclusive:void 0)||false;scrollLock=(options!=null?options.scrollLock:void 0)||false;closable=(options!=null?options.closable:void 0)||true;pusher=document.querySelector('.pusher');closeVisibleSidebars=function(){ts('.ts.sidebar.visible:not(.static)').addClass('animating').removeClass('visible').one(animationEnd,function(){return ts(this).removeClass('animating');});return ts('.pusher').removeClass('dimmed').removeAttr('data-pusher-lock');};if(pusher.getAttribute('data-closable-bind')!=='true'){pusher.addEventListener('click',function(e){if(pusher.getAttribute('data-sidebar-closing')!=='true'){return closeVisibleSidebars();}});}
|
||||
pusher.setAttribute('data-closable-bind',true);return this.each(function(){var that;if(options==='toggle'||options==='hide'||options==='show'){ts(this).addClass('animating');pusher.setAttribute('data-sidebar-closing','true');setTimeout(function(){return pusher.removeAttribute('data-sidebar-closing');},300);if(this.getAttribute('data-dim-page')===null){this.setAttribute('data-dim-page',dimPage);}
|
||||
if(this.getAttribute('data-scroll-lock')===null){this.setAttribute('data-scroll-lock',scrollLock);}
|
||||
if(!ts(this).hasClass('visible')&&options==='hide'){ts(this).removeClass('animating');}
|
||||
if((ts(this).hasClass('visible')&&options==='toggle')||options==='hide'){ts('.pusher').removeClass('dimmed').removeAttr('data-pusher-lock');return ts(this).removeClass('visible').one(animationEnd,function(){return ts(this).removeClass('animating');});}else{if(this.getAttribute('data-exclusive')==='true'){closeVisibleSidebars();}
|
||||
if(this.getAttribute('data-dim-page')==='true'){ts('.pusher').addClass('dimmed');}
|
||||
if(this.getAttribute('data-scroll-lock')==='true'){ts('.pusher').attr('data-pusher-lock','true');}
|
||||
return ts(this).addClass('visible').removeClass('animating');}}else if(options==='attach events'){that=this;switch(eventName){case'show':return ts(selector).attr('data-sidebar-trigger','true').on('click',function(){return ts(that).sidebar('show');});case'hide':return ts(selector).attr('data-sidebar-trigger','true').on('click',function(){return ts(that).sidebar('hide');});case'toggle':return ts(selector).attr('data-sidebar-trigger','true').on('click',function(){return ts(that).sidebar('toggle');});}}else if(typeof options==='object'){this.setAttribute('data-closable',closable);this.setAttribute('data-scroll-lock',scrollLock);this.setAttribute('data-exclusive',exclusive);return this.setAttribute('data-dim-page',dimPage);}});};ts.fn.tab=function(option){return this.each(function(){var onSwitch;onSwitch=(option!=null?option.onSwitch:void 0)||function(){};return ts(this).on('click',function(){var tabGroup,tabName;if(ts(this).hasClass('active')){return;}
|
||||
tabName=ts(this).attr('data-tab');if(tabName===null){return;}
|
||||
tabGroup=ts(this).attr('data-tab-group');onSwitch(tabName,tabGroup);if(tabGroup===null){ts('[data-tab]:not(.tab):not([data-tab-group])').removeClass('active');ts('[data-tab]:not([data-tab-group])').removeClass('active');ts(`.tab[data-tab='${tabName}']:not([data-tab-group])`).addClass('active');}else{ts(`[data-tab-group='${tabGroup}']:not(.tab)`).removeClass('active');ts(`.tab[data-tab-group='${tabGroup}']`).removeClass('active');ts(`.tab[data-tab='${tabName}'][data-tab-group='${tabGroup}']`).addClass('active');}
|
||||
return ts(this).addClass('active');});});};ts.fn.popup=function(){return this.each(function(){var android,iOS,userAgent,winPhone;userAgent=navigator.userAgent||navigator.vendor||window.opera;winPhone=new RegExp("windows phone","i");android=new RegExp("android","i");iOS=new RegExp("iPad|iPhone|iPod","i");if(winPhone.test(userAgent)||android.test(userAgent)||(iOS.test(userAgent)&&!window.MSStream)){return ts(this).addClass('untooltipped');}});};ts.fn.slider=function(option){var counter,modify,outerCounter;outerCounter=option!=null?option.outerCounter:void 0;counter=option!=null?option.counter:void 0;modify=function(sliderEl,inputEl,counter,outerCounter){var counterEl,value;value=(inputEl.value-inputEl.getAttribute('min'))/(inputEl.getAttribute('max'-inputEl.getAttribute('min')));if(value===Number.POSITIVE_INFINITY){value=inputEl.value / 100;}
|
||||
if(counter!=null){counterEl=ts(sliderEl).find(counter);if(counterEl!=null){counterEl[0].innerText=inputEl.value;}}
|
||||
if(outerCounter!=null){ts(outerCounter).innerText=inputEl.value;}
|
||||
return ts(inputEl).css('background-image',`-webkit-gradient(linear,left top,right top,color-stop(${value},${slider_progressColor}),color-stop(${value},${slider_trackColor}))`);};return this.each(function(){var inputEl,sliderEl;sliderEl=this;inputEl=ts(this).find('input[type="range"]');modify(this,inputEl[0],counter,outerCounter);return inputEl.on('input',function(){return modify(sliderEl,this,counter,outerCounter);});});};ts.fn.editable=function(option){var autoClose,autoReplace,inputWrapper,onEdit,onEdited;autoReplace=(option!=null?option.autoReplace:void 0)||true;onEdit=(option!=null?option.onEdit:void 0)||function(){};onEdited=(option!=null?option.onEdited:void 0)||function(){};autoClose=(option!=null?option.autoClose:void 0)||true;inputWrapper=this;if(autoClose){ts(document).on('click',function(event){if(ts(event.target).closest('.ts.input')===null){return inputWrapper.each(function(){var contenteditable,input,text;input=ts(this).find('input');contenteditable=ts(this).find('[contenteditable]');text=ts(this).find('.text')[0];if(autoReplace){if(input!=null){text.innerText=input[0].value;}else if(contenteditable!=null){text.innerText=contenteditable[0].value;}}
|
||||
onEdited(this);return ts(this).removeClass('editing');});}});}
|
||||
return this.each(function(){var contenteditable,input;input=ts(this).find('input');contenteditable=ts(this).find('[contenteditable]');return ts(this).on('click',function(){ts(this).addClass('editing');onEdit(this);if(input!=null){return input[0].focus();}else if(contenteditable!=null){return contenteditable[0].focus();}});});};ts.fn.message=function(){return this.each(function(){return ts(this).find('i.close').on('click',function(){return ts(this).closest('.ts.message').addClass('hidden');});});};ts.fn.snackbar=function(option){var action,actionEmphasis,content,hoverStay,interval,onAction,onClose;content=(option!=null?option.content:void 0)||null;action=(option!=null?option.action:void 0)||null;actionEmphasis=(option!=null?option.actionEmphasis:void 0)||null;onClose=(option!=null?option.onClose:void 0)||function(){};onAction=(option!=null?option.onAction:void 0)||function(){};hoverStay=(option!=null?option.hoverStay:void 0)||false;interval=3500;if(content===null){return;}
|
||||
return this.each(function(){var ActionEl,close,contentEl,snackbar;snackbar=this;contentEl=ts(snackbar).find('.content');ActionEl=ts(snackbar).find('.action');ts(snackbar).removeClass('active animating').addClass('active animating').one(animationEnd,function(){return ts(this).removeClass('animating');}).attr('data-mouseon','false');contentEl[0].innerText=content;if(ActionEl!=null){ActionEl[0].innerText=action;}
|
||||
if((actionEmphasis!=null)&&(ActionEl!=null)){ActionEl.removeClass('primary info warning negative positive').addClass(actionEmphasis);}
|
||||
close=function(){ts(snackbar).removeClass('active').addClass('animating').one(animationEnd,function(){ts(this).removeClass('animating');return onClose(snackbar,content,action);});return clearTimeout(snackbar.snackbarTimer);};if(ActionEl!=null){ActionEl.off('click');ActionEl.on('click',function(){close();return onAction(snackbar,content,action);});}
|
||||
if(hoverStay){ts(snackbar).on('mouseenter',function(){return ts(this).attr('data-mouseon','true');});ts(snackbar).on('mouseleave',function(){return ts(this).attr('data-mouseon','false');});}
|
||||
clearTimeout(snackbar.snackbarTimer);return snackbar.snackbarTimer=setTimeout(function(){var hoverChecker;if(hoverStay){return hoverChecker=setInterval(function(){if(ts(snackbar).attr('data-mouseon')==='false'){close();return clearInterval(hoverChecker);}},600);}else{return close();}},interval);});};ts.fn.contextmenu=function(option){var menu;menu=(option!=null?option.menu:void 0)||null;ts(document).on('click',function(event){return ts('.ts.contextmenu.visible').removeClass('visible').addClass('hidden animating').one(animationEnd,function(){return ts(this).removeClass('visible animating downward upward rightward leftward');});});return this.each(function(){return ts(this).on('contextmenu',function(e){var h,r,w;event.preventDefault();ts(menu).addClass('visible');r=ts(menu)[0].getBoundingClientRect();ts(menu).removeClass('visible');w=window.innerWidth / 2;h=window.innerHeight / 2;ts(menu).removeClass('downward upward rightward leftward');if(e.clientX<w&&e.clientY<h){ts(menu).addClass('downward rightward').css('left',e.clientX+'px').css('top',e.clientY+'px');}else if(e.clientX<w&&e.clientY>h){ts(menu).addClass('upward rightward').css('left',e.clientX+'px').css('top',e.clientY-r.height+'px');}else if(e.clientX>w&&e.clientY>h){ts(menu).addClass('upward leftward').css('left',e.clientX-r.width+'px').css('top',e.clientY-r.height+'px');}else if(e.clientX>w&&e.clientY<h){ts(menu).addClass('downward leftward').css('left',e.clientX-r.width+'px').css('top',e.clientY+'px');}
|
||||
return ts(menu).removeClass('hidden').addClass('visible animating').one(animationEnd,function(){return ts(this).removeClass('animating');});});});};ts.fn.embed=function(option){return this.each(function(){var embedEl,icon,iconEl,id,options,placeholder,placeholderEl,query,source,url;source=this.getAttribute('data-source');url=this.getAttribute('data-url');id=this.getAttribute('data-id');placeholder=this.getAttribute('data-placeholder');options=this.getAttribute('data-options')||'';query=this.getAttribute('data-query')||'';icon=this.getAttribute('data-icon')||'video play';embedEl=this;if(this.getAttribute('data-embed-actived')){return;}
|
||||
if(query!==''){query='?'+query;}
|
||||
if(placeholder){placeholderEl=document.createElement('img');placeholderEl.src=placeholder;placeholderEl.className='placeholder';this.appendChild(placeholderEl);}
|
||||
if(icon&&(source||url||id)){iconEl=document.createElement('i');iconEl.className=icon+' icon';ts(iconEl).on('click',function(){var iframeEl,urlExtension,videoEl;urlExtension=url?url.split('.').pop():'';if(urlExtension.toUpperCase().indexOf('MOV')!==-1||urlExtension.toUpperCase().indexOf('MP4')!==-1||urlExtension.toUpperCase().indexOf('WEBM')!==-1||urlExtension.toUpperCase().indexOf('OGG')!==-1){videoEl=document.createElement('video');videoEl.src=url;if(options!==''){options.split(',').forEach(function(pair){var key,p,value;p=pair.split('=');key=p[0];value=p[1]||'';return videoEl.setAttribute(key.trim(),value.trim());});}
|
||||
ts(embedEl).addClass('active');return embedEl.appendChild(videoEl);}else{iframeEl=document.createElement('iframe');iframeEl.width='100%';iframeEl.height='100%';iframeEl.frameborder='0';iframeEl.scrolling='no';iframeEl.setAttribute('webkitAllowFullScreen','');iframeEl.setAttribute('mozallowfullscreen','');iframeEl.setAttribute('allowFullScreen','');if(source){switch(source){case'youtube':iframeEl.src='https://www.youtube.com/embed/'+id+query;break;case'vimeo':iframeEl.src='https://player.vimeo.com/video/'+id+query;}}else if(url){iframeEl.src=url+query;}
|
||||
ts(embedEl).addClass('active');return embedEl.appendChild(iframeEl);}});this.appendChild(iconEl);}
|
||||
return this.setAttribute('data-embed-actived','true');});};ts.fn.accordion=function(){};ts.fn.scrollspy=function(options){var anchors,container,target,tsTarget;target=document.querySelector(options.target);tsTarget=ts(target);container=this[0];anchors=document.querySelectorAll(`[data-scrollspy='${target.id}']`);if(this[0]===document.body){container=document;}
|
||||
return Array.from(anchors).forEach(function(element,index,array){var anchor,event,link;anchor=element;link=`[href='#${anchor.id}']`;event=function(){var containerRect,containerTop,continerIsBottom,length,rect;rect=anchor.getBoundingClientRect();if(container===document){containerRect=document.documentElement.getBoundingClientRect();continerIsBottom=document.body.scrollHeight-(document.body.scrollTop+window.innerHeight)===0;}else{containerRect=container.getBoundingClientRect();continerIsBottom=container.scrollHeight-(container.scrollTop+container.clientHeight)===0;}
|
||||
containerTop=containerRect.top<0?0:containerRect.top;if(rect.top-containerTop<10||(continerIsBottom&&(index===array.length-1))){tsTarget.find(link).addClass('active');length=tsTarget.find('.active').length;return tsTarget.find('.active').each(function(index){if(index!==length-1){return ts(this).removeClass('active');}});}else{return tsTarget.find(link).removeClass('active');}};event.call(this);container.addEventListener('scroll',event);return window.addEventListener('hashchange',event);});};
|
127
include/js/lib/zh-tw.js
Normal file
127
include/js/lib/zh-tw.js
Normal file
@ -0,0 +1,127 @@
|
||||
(function(){
|
||||
var factory = function (exports) {
|
||||
var lang = {
|
||||
name : "zh-tw",
|
||||
description : "開源在線Markdown編輯器<br/>Open source online Markdown editor.",
|
||||
tocTitle : "選單",
|
||||
toolbar : {
|
||||
undo : "復原(Ctrl+Z)",
|
||||
redo : "重做(Ctrl+Y)",
|
||||
bold : "粗體",
|
||||
del : "刪除線",
|
||||
italic : "斜體",
|
||||
quote : "引用",
|
||||
ucwords : "將所選的每個單字首字母轉成大寫",
|
||||
uppercase : "將所選文字轉成大寫",
|
||||
lowercase : "將所選文字轉成小寫",
|
||||
h1 : "標題1",
|
||||
h2 : "標題2",
|
||||
h3 : "標題3",
|
||||
h4 : "標題4",
|
||||
h5 : "標題5",
|
||||
h6 : "標題6",
|
||||
"list-ul" : "無序清單",
|
||||
"list-ol" : "有序清單",
|
||||
hr : "分隔線",
|
||||
link : "連結",
|
||||
"reference-link" : "引用連結",
|
||||
image : "圖片",
|
||||
code : "行內代碼",
|
||||
"preformatted-text" : "預格式文本 / 代碼塊(縮進風格)",
|
||||
"code-block" : "代碼塊(多語言風格)",
|
||||
table : "添加表格",
|
||||
datetime : "日期時間",
|
||||
emoji : "Emoji 表情",
|
||||
"html-entities" : "HTML 實體字符",
|
||||
pagebreak : "插入分頁符",
|
||||
watch : "關閉實時預覽",
|
||||
unwatch : "開啟實時預覽",
|
||||
preview : "預覽(按 Shift + ESC 退出)",
|
||||
fullscreen : "全螢幕(按 ESC 退出)",
|
||||
clear : "清空",
|
||||
search : "搜尋",
|
||||
help : "幫助",
|
||||
info : "關於" + exports.title
|
||||
},
|
||||
buttons : {
|
||||
enter : "確定",
|
||||
cancel : "取消",
|
||||
close : "關閉"
|
||||
},
|
||||
dialog : {
|
||||
link : {
|
||||
title : "添加連結",
|
||||
url : "連結位址",
|
||||
urlTitle : "連結標題",
|
||||
urlEmpty : "錯誤:請填寫連結位址。"
|
||||
},
|
||||
referenceLink : {
|
||||
title : "添加引用連結",
|
||||
name : "引用名稱",
|
||||
url : "連結位址",
|
||||
urlId : "連結ID",
|
||||
urlTitle : "連結標題",
|
||||
nameEmpty: "錯誤:引用連結的名稱不能為空。",
|
||||
idEmpty : "錯誤:請填寫引用連結的ID。",
|
||||
urlEmpty : "錯誤:請填寫引用連結的URL地址。"
|
||||
},
|
||||
image : {
|
||||
title : "添加圖片",
|
||||
url : "圖片位址",
|
||||
link : "圖片連結",
|
||||
alt : "圖片描述",
|
||||
uploadButton : "本地上傳",
|
||||
imageURLEmpty : "錯誤:圖片地址不能為空。",
|
||||
uploadFileEmpty : "錯誤:上傳的圖片不能為空!",
|
||||
formatNotAllowed : "錯誤:只允許上傳圖片文件,允許上傳的圖片文件格式有:"
|
||||
},
|
||||
preformattedText : {
|
||||
title : "添加預格式文本或代碼塊",
|
||||
emptyAlert : "錯誤:請填寫預格式文本或代碼的內容。"
|
||||
},
|
||||
codeBlock : {
|
||||
title : "添加代碼塊",
|
||||
selectLabel : "代碼語言:",
|
||||
selectDefaultText : "請語言代碼語言",
|
||||
otherLanguage : "其他語言",
|
||||
unselectedLanguageAlert : "錯誤:請選擇代碼所屬的語言類型。",
|
||||
codeEmptyAlert : "錯誤:請填寫代碼內容。"
|
||||
},
|
||||
htmlEntities : {
|
||||
title : "HTML實體字符"
|
||||
},
|
||||
help : {
|
||||
title : "幫助"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.defaults.lang = lang;
|
||||
};
|
||||
|
||||
// CommonJS/Node.js
|
||||
if (typeof require === "function" && typeof exports === "object" && typeof module === "object")
|
||||
{
|
||||
module.exports = factory;
|
||||
}
|
||||
else if (typeof define === "function") // AMD/CMD/Sea.js
|
||||
{
|
||||
if (define.amd) { // for Require.js
|
||||
|
||||
define(["editormd"], function(editormd) {
|
||||
factory(editormd);
|
||||
});
|
||||
|
||||
} else { // for Sea.js
|
||||
define(function(require) {
|
||||
var editormd = require("../editormd");
|
||||
factory(editormd);
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
factory(window.editormd);
|
||||
}
|
||||
|
||||
})();
|
46
include/js/like.js
Normal file
46
include/js/like.js
Normal file
@ -0,0 +1,46 @@
|
||||
$('#content').on('click', 'button.like.button', function(e){
|
||||
var el = e.currentTarget;
|
||||
var id = el.dataset.id;
|
||||
axios.request({
|
||||
method: "GET",
|
||||
url: "./ajax/like.php?pid=" + id,
|
||||
responseType: "json",
|
||||
}).then(function (res) {
|
||||
var data = res.data;
|
||||
if (data.status == true) {
|
||||
$(`button.like.button[data-id="${data.id}"]`).html(
|
||||
'<i class="thumbs up icon"></i> ' + data.likes
|
||||
);
|
||||
} else if (data.status == false) {
|
||||
$(`button.like.button[data-id="${data.id}"]`).html(
|
||||
'<i class="thumbs outline up icon"></i> ' + data.likes
|
||||
);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
if (error.response) {
|
||||
let data = error.response.data;
|
||||
if (data.status == 'nologin') {
|
||||
$(`button.like.button[data-id="${data.id}"]`).html(
|
||||
'<i class="thumbs outline up icon"></i> ' + data.likes
|
||||
);
|
||||
swal({
|
||||
type: 'warning',
|
||||
title: '請先登入!',
|
||||
text: '登入以按讚或發表留言。',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '登入',
|
||||
cancelButtonText: '取消',
|
||||
}).then((result) => {
|
||||
if (result.value) { // confirm
|
||||
location.href = 'login.php';
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$(`button.like.button[data-id="${id}"]`).html(
|
||||
'<i class="thumbs outline up icon"></i> ' + "--"
|
||||
);
|
||||
console.error(`An error occurred when get likes of pid ${id}, status ${error.response.status}`);
|
||||
}
|
||||
});
|
||||
});
|
40
include/js/markdown.js
Normal file
40
include/js/markdown.js
Normal file
@ -0,0 +1,40 @@
|
||||
editormd.urls = {
|
||||
atLinkBase : "user.php?username="
|
||||
};
|
||||
|
||||
function postProcess(...callbacks) {
|
||||
tableStyling();
|
||||
linkSanitize();
|
||||
|
||||
callbacks.forEach(func => {
|
||||
func();
|
||||
});
|
||||
|
||||
function linkSanitize() {
|
||||
$('.markdown-body a').each((_i, e) => {
|
||||
href = (e.getAttribute('href')) ? _.unescape(e.getAttribute('href').toLowerCase()) : "";
|
||||
if (href.indexOf('javascript:') != -1) {
|
||||
e.setAttribute('href', '#');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tableStyling() {
|
||||
$('table').each((_i,e) => {
|
||||
$(e).addClass("ts celled table").css('display', 'table').wrap('<div class="table wrapper"></div>');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parseMarkdown(id, markdown, options) {
|
||||
let defaultOptions = {
|
||||
htmlDecode : "script,iframe|on*",
|
||||
toc: true,
|
||||
emoji: true,
|
||||
taskList: true,
|
||||
tex: true,
|
||||
flowChart: true,
|
||||
sequenceDiagram: true
|
||||
}
|
||||
return editormd.markdownToHTML(id, $.extend(true, defaultOptions, options, { markdown: markdown }));
|
||||
}
|
126
include/js/notification.js
Normal file
126
include/js/notification.js
Normal file
@ -0,0 +1,126 @@
|
||||
var notifications = {
|
||||
toFetch: true,
|
||||
unreadCount: 0,
|
||||
feeds: []
|
||||
};
|
||||
|
||||
$('#menu .notification.icon.item').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
let el = e.currentTarget;
|
||||
|
||||
let $wrapper = $('#notification-wrapper');
|
||||
let $container = $('.notification.container');
|
||||
|
||||
if ($container.hasClass('active')) {
|
||||
// dismiss the notification window
|
||||
$('.notification.click.handler').remove();
|
||||
} else {
|
||||
// render the notification window
|
||||
let handler = document.createElement('div')
|
||||
handler.className = "notification click handler";
|
||||
$wrapper.after(handler);
|
||||
handler.addEventListener('click', function (e) {
|
||||
el.click();
|
||||
});
|
||||
setNotificationCounter(0); // remove counter
|
||||
if (notifications.toFetch){
|
||||
fetchNotification();
|
||||
}
|
||||
}
|
||||
|
||||
el.classList.toggle('active');
|
||||
$container.toggleClass('active');
|
||||
});
|
||||
|
||||
function fetchNotificationCount() {
|
||||
axios.request({
|
||||
method: 'GET',
|
||||
url: "./ajax/notification.php?count",
|
||||
responseType: 'json'
|
||||
}).then(function (res) {
|
||||
let count = res.data['unread_count'];
|
||||
setNotificationCounter(count);
|
||||
if (count != notifications.unreadCount) {
|
||||
// if count changes, fetching notifications while next click
|
||||
notifications.toFetch = true;
|
||||
notifications.unreadCount = count;
|
||||
}
|
||||
}).catch(function (_error) {
|
||||
console.error("Error occurred while fetching notification count.");
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
fetchNotificationCount();
|
||||
});
|
||||
var notificationFetchTimer = setInterval(fetchNotificationCount, 1 * 5 * 1000); // fetch notification count every 1 minute
|
||||
|
||||
function fetchNotification() {
|
||||
axios.request({
|
||||
method: 'GET',
|
||||
url: './ajax/notification.php?fetch',
|
||||
responseType: 'json'
|
||||
}).then(function (res) {
|
||||
parseNotification(res.data);
|
||||
notifications.toFetch = false;
|
||||
}).catch(function (error) {
|
||||
console.log("Error occurred while fetching notification count.");
|
||||
});
|
||||
}
|
||||
|
||||
function parseNotification(data) {
|
||||
const feedTemplate = `<div class="event"><div class="label"><i class="volume up icon"></i></div><div class="content"><div class="date">{{ time }}</div><div class="summary">{{ message }}</div></div></div>`;
|
||||
let $feed = $('.ts.feed');
|
||||
|
||||
$feed.html(""); // container clean up
|
||||
|
||||
for (f of data.feeds) {
|
||||
let message = parseMessage(f.message, f.url);
|
||||
let node = feedTemplate.replace("{{ time }}", f.time).replace("{{ message }}", message);
|
||||
$node = $(node).appendTo($feed);
|
||||
|
||||
if (f.read == 0) {
|
||||
$node.addClass('unread');
|
||||
}
|
||||
}
|
||||
|
||||
notifications.feeds = data.feeds; // cache data
|
||||
|
||||
function parseMessage(message, url) {
|
||||
let regex = {
|
||||
"username": /\{([^\{\}]+)\}@(\w+)/g,
|
||||
"url": /\[([^\[\[]*)\]/g
|
||||
};
|
||||
|
||||
return message.replace(regex.username, function (_match, name, id, _offset, _string) {
|
||||
return `<a href="user.php?username=${id}">${name}</a>`;
|
||||
}).replace(regex.url, function (_match, title, _offset, _string) {
|
||||
return `<a href="${url}">${title}</a>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setNotificationCounter(count) {
|
||||
let $notify = $('#menu .notification.icon.item');
|
||||
let $icon = $notify.children('i.icon');
|
||||
let $counter = $notify.children('span.counter');
|
||||
|
||||
if (count == 0) {
|
||||
if ($counter.length) {
|
||||
$counter.remove();
|
||||
}
|
||||
$icon.toggleClass('outline', true); // set icon style
|
||||
} else {
|
||||
if ($counter.length) {
|
||||
$counter.text(count);
|
||||
} else {
|
||||
let counter = document.createElement('span');
|
||||
counter.className = "counter";
|
||||
counter.textContent = count;
|
||||
$notify.append(counter);
|
||||
}
|
||||
$icon.toggleClass('outline', false); // set icon style
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
86
include/js/post.js
Normal file
86
include/js/post.js
Normal file
@ -0,0 +1,86 @@
|
||||
const cdnjs = "https://cdnjs.cloudflare.com/ajax/libs";
|
||||
|
||||
// Load Libraries
|
||||
const libraries = [
|
||||
cdnjs + "/marked/0.5.1/marked.min.js",
|
||||
cdnjs + "/prettify/r298/prettify.min.js",
|
||||
cdnjs + "/raphael/2.2.7/raphael.min.js",
|
||||
cdnjs + "/underscore.js/1.9.1/underscore-min.js",
|
||||
cdnjs + "/flowchart/1.11.3/flowchart.min.js",
|
||||
"https://pandao.github.io/editor.md/lib/jquery.flowchart.min.js",
|
||||
cdnjs + "/js-sequence-diagrams/1.0.6/sequence-diagram-min.js"
|
||||
];
|
||||
|
||||
loadJS(libraries).then(function () {
|
||||
editormd.$marked = marked;
|
||||
editormd.loadFiles.js.push(...libraries.map(url => url.slice(0, -3))); // remove ".js"
|
||||
parsePost();
|
||||
fetchComments();
|
||||
postProcess(sanitizeStyleTag());
|
||||
|
||||
function sanitizeStyleTag() { // prevent the style tag in post from destorying the style of page
|
||||
return function() {
|
||||
var parser = new cssjs();
|
||||
let stylesheets = document.querySelectorAll('#post style');
|
||||
for (let style of stylesheets) {
|
||||
let ruleSource = style.innerHTML;
|
||||
let cssObject = parser.parseCSS(ruleSource);
|
||||
for (let rule of cssObject) {
|
||||
let valid = false;
|
||||
let validPrefix = ["#post ", "#post.markdown-body ", "#post.editormd-html-preview "];
|
||||
validPrefix.forEach((e, _i) => {
|
||||
valid = valid || rule.selector.startsWith(e);
|
||||
});
|
||||
|
||||
if (!rule.selector.startsWith('@')) { // '@keyframe' & '@import'
|
||||
if (valid) {
|
||||
// do nothing
|
||||
} else if (rule.selector.startsWith('.markdown-body ') || rule.selector.startsWith(".editormd-html-preview")) {
|
||||
rule.selector = "#post" + rule.selector;
|
||||
} else {
|
||||
rule.selector = "#post " + rule.selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
style.innerHTML = parser.getCSSForEditor(cssObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function parsePost() {
|
||||
var postContent = document.querySelector('#post .markdown').textContent;
|
||||
|
||||
if (postContent.search(/.{0}\[TOC\]\n/) != -1) { // if TOC is used in post
|
||||
$('#sidebar .ts.fluid.input').after(`<div class="ts tertiary top attached center aligned segment">目錄</div><div class="ts bottom attached loading segment" id="toc"></div>`);
|
||||
}
|
||||
|
||||
parseMarkdown('post', postContent, {
|
||||
tocDropdown: false,
|
||||
tocContainer: '#toc'
|
||||
}).children('.markdown').hide();
|
||||
$('#toc').removeClass('loading');
|
||||
}
|
||||
|
||||
// Delete post confirm message
|
||||
$('.action.column .delete').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var el = this;
|
||||
var next = el.getAttribute('href');
|
||||
swal({
|
||||
type: 'question',
|
||||
title: '確定要刪除嗎?',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '確定',
|
||||
cancelButtonText: '取消',
|
||||
}).then((result) => {
|
||||
if (result.value) { // confirm
|
||||
axios.request({
|
||||
method: "GET",
|
||||
url: next
|
||||
}).then(function (res) {
|
||||
location.href = res.headers["axios-location"];
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
19
include/js/security.js
Normal file
19
include/js/security.js
Normal file
@ -0,0 +1,19 @@
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
axios.interceptors.request.use(function (config) {
|
||||
var crypto = window.crypto || window.msCrypto;
|
||||
let csrfToken = btoa(String(crypto.getRandomValues(new Uint32Array(1))[0]));
|
||||
document.cookie = `${axios.defaults.xsrfCookieName}=${csrfToken}`;
|
||||
return config;
|
||||
}, function (error) {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
$("#logout").on("click", function (e) {
|
||||
e.preventDefault();
|
||||
axios.get("login.php?logout").then(function (res) {
|
||||
location.href = res.headers["axios-location"];
|
||||
}).catch(function (error) {
|
||||
console.log(error);
|
||||
});
|
||||
});
|
21
include/notification.php
Normal file
21
include/notification.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
function cavern_notify_user($username, $message="你有新的通知!", $url="", $type="") {
|
||||
global $SQL;
|
||||
$time = date('Y-m-d H:i:s');
|
||||
$SQL->query("INSERT INTO `notification` (`username`, `message`, `url`, `type`, `time`) VALUES ('%s', '%s', '%s', '%s', '%s')", array($username, $message, $url, $type, $time));
|
||||
}
|
||||
|
||||
function parse_user_tag($markdown) {
|
||||
$regex = array(
|
||||
"code_block" => "/(`{1,3}[^`]*`{1,3})/",
|
||||
"email" => "/[^@\s]*@[^@\s]*\.[^@\s]*/",
|
||||
"username" => "/@(\w+)/"
|
||||
);
|
||||
|
||||
$tmp = preg_replace($regex["code_block"], " ", $markdown);
|
||||
$tmp = preg_replace($regex["email"], " ", $tmp);
|
||||
|
||||
preg_match_all($regex["username"], $tmp, $username_list);
|
||||
|
||||
return array_unique($username_list[1]);
|
||||
}
|
9
include/security.php
Normal file
9
include/security.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
function validate_csrf() {
|
||||
if (isset($_COOKIE["XSRF-TOKEN"]) && isset($_SERVER["HTTP_X_XSRF_TOKEN"]) && ($_COOKIE["XSRF-TOKEN"] === $_SERVER["HTTP_X_XSRF_TOKEN"])) {
|
||||
return TRUE;
|
||||
} else {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
?>
|
65
include/user.php
Normal file
65
include/user.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
class NoUserException extends Exception {}
|
||||
|
||||
class User {
|
||||
private $valid = false;
|
||||
private $islogin = false;
|
||||
|
||||
private $username;
|
||||
private $name;
|
||||
private $level;
|
||||
private $muted;
|
||||
private $email;
|
||||
|
||||
public function __construct($username="") {
|
||||
// if $username is empty indicates that user is not logged in
|
||||
if ($username !== "") {
|
||||
// the user might be banned or removed, so we validate him here
|
||||
$query = cavern_query_result("SELECT * FROM `user` WHERE `username` = '%s'", array($username));
|
||||
|
||||
if ($query['num_rows'] > 0){
|
||||
$this->valid = true;
|
||||
|
||||
$data = $query['row'];
|
||||
$this->username = $data["username"];
|
||||
$this->name = $data['name'];
|
||||
$this->level = $data['level'];
|
||||
$this->muted = ($data['muted'] == 1 ? true : false);
|
||||
$this->email = $data['email'];
|
||||
} else {
|
||||
throw new NoUserException($username);
|
||||
}
|
||||
|
||||
if ($this->username === @$_SESSION["cavern_username"]) {
|
||||
$this->islogin = true;
|
||||
}
|
||||
} else {
|
||||
// even though the user hasn't logged in, he is still a valid user
|
||||
$this->username = "";
|
||||
$this->valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function __get($name) {
|
||||
return $this->$name;
|
||||
}
|
||||
}
|
||||
|
||||
function validate_user() {
|
||||
if (isset($_SESSION['cavern_username'])) {
|
||||
$username = $_SESSION['cavern_username'];
|
||||
} else {
|
||||
$username = "";
|
||||
}
|
||||
|
||||
try {
|
||||
$user = new User($username);
|
||||
} catch (NoUserException $e) {}
|
||||
|
||||
if (!$user->valid) {
|
||||
session_destroy();
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
?>
|
100
include/view.php
Normal file
100
include/view.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/* Cavern Edition
|
||||
modified by t510599 at 2019/05/30
|
||||
*/
|
||||
/*
|
||||
<Secret Blog>
|
||||
Copyright (C) 2012-2017 太陽部落格站長 Secret <http://gdsecret.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, version 3.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
class View {
|
||||
private $master_content;
|
||||
private $nav_content;
|
||||
private $sidebar_content;
|
||||
private $message = array();
|
||||
private $script = array();
|
||||
private $title;
|
||||
private $part;
|
||||
|
||||
public function __construct($master,$nav,$sidebar,$title,$part) {
|
||||
$this->load($master,$nav,$sidebar);
|
||||
$this->title = $title;
|
||||
$this->part = $part;
|
||||
ob_start();
|
||||
}
|
||||
|
||||
private function load($master,$nav,$sidebar) {
|
||||
ob_start();
|
||||
include($master);
|
||||
$this->master_content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
ob_start();
|
||||
include($nav);
|
||||
$this->nav_content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
if ($sidebar!='') {
|
||||
ob_start();
|
||||
include($sidebar);
|
||||
$this->sidebar_content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
public function add_script($src) {
|
||||
$this->script[] = "<script src=\"$src\"></script>";
|
||||
}
|
||||
|
||||
public function add_script_source($source) {
|
||||
$this->script[] = "<script>$source</script>";
|
||||
}
|
||||
|
||||
public function show_message($class, $msg) {
|
||||
$this->message[] = "<div class=\"ts $class message\"><p>$msg</p></div>";
|
||||
}
|
||||
|
||||
public function render() {
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
echo strtr($this->master_content, array(
|
||||
'{title}' => $this->title,
|
||||
'{part}' => $this->part,
|
||||
'{script}' => join(PHP_EOL, $this->script),
|
||||
'{nav}' => $this->nav_content,
|
||||
'{sidebar}' => $this->sidebar_content,
|
||||
'{message}' => join(PHP_EOL, $this->message),
|
||||
'{content}' => $content
|
||||
));
|
||||
@ob_flush();
|
||||
flush();
|
||||
}
|
||||
};
|
132
index.php
Normal file
132
index.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
require_once('connection/SQL.php');
|
||||
require_once('config.php');
|
||||
require_once('include/view.php');
|
||||
require_once('include/user.php');
|
||||
require_once('include/article.php');
|
||||
|
||||
$user = validate_user();
|
||||
if (!$user->valid) {
|
||||
http_response_code(403);
|
||||
header("Location: index.php?err=account");
|
||||
exit;
|
||||
}
|
||||
|
||||
$all_posts_count = cavern_query_result("SELECT COUNT(*) AS `count` FROM `post`")['row']['count'];
|
||||
|
||||
if (isset($_GET['page']) && trim($_GET['page']) != "") {
|
||||
$limit_start = abs((intval($_GET['page']) - 1) * $blog['limit']);
|
||||
if ($limit_start > $all_posts_count) { // we don't have that much posts
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$limit_start = 0;
|
||||
}
|
||||
|
||||
$post_list = article_list(cavern_query_result(
|
||||
"SELECT `post`.*, `user`.name FROM `post` INNER JOIN `user` ON `post`.username = `user`.username ORDER BY `time` DESC LIMIT %d,%d",
|
||||
array($limit_start, $blog['limit']))
|
||||
);
|
||||
|
||||
if ($user->islogin) {
|
||||
$view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], "首頁");
|
||||
$view->add_script_source("ts('.ts.dropdown:not(.basic)').dropdown();");
|
||||
} else {
|
||||
$view = new View('theme/default.html', 'theme/nav/default.html', 'theme/sidebar.php', $blog['name'], "首頁");
|
||||
}
|
||||
|
||||
$view->add_script("https://unpkg.com/load-js@1.2.0");
|
||||
$view->add_script("./include/js/lib/editormd.js");
|
||||
$view->add_script("./include/js/security.js");
|
||||
$view->add_script('./include/js/markdown.js');
|
||||
$view->add_script('./include/js/cards.js');
|
||||
$view->add_script('./include/js/like.js');
|
||||
|
||||
// ok message
|
||||
if (isset($_GET['ok'])) {
|
||||
switch ($_GET['ok']) {
|
||||
case 'login':
|
||||
if ($user->islogin) {
|
||||
// only show welcome message if user is logged in
|
||||
$greeting = cavern_greeting();
|
||||
$view->show_message('inverted positive', "{$greeting}!我的朋友!");
|
||||
}
|
||||
break;
|
||||
case 'reg':
|
||||
$view->show_message('inverted primary', '註冊成功');
|
||||
break;
|
||||
case 'logout':
|
||||
if (!$user->islogin) {
|
||||
// only show message if user is logged out
|
||||
$view->show_message('inverted info', '已登出');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// error message
|
||||
if (isset($_GET['err'])) {
|
||||
switch ($_GET['err']) {
|
||||
case 'account':
|
||||
$view->show_message('inverted negative', '帳號不存在');
|
||||
break;
|
||||
case 'login':
|
||||
$view->show_message('inverted negative', '帳號或密碼錯誤');
|
||||
break;
|
||||
case 'permission':
|
||||
$view->show_message('warning', '帳號權限不足');
|
||||
break;
|
||||
case 'post':
|
||||
$view->show_message('negative', '找不到文章');
|
||||
break;
|
||||
case 'nologin':
|
||||
$view->show_message('warning', '請先登入');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sizeOf($post_list) > 0) { ?>
|
||||
<div class="ts active big text loader">載入中</div>
|
||||
<div class="ts loading flatted borderless centered segment" id="cards">
|
||||
<?php
|
||||
foreach ($post_list as $_key => $article) {
|
||||
?>
|
||||
|
||||
<div class="ts card" data-id="<?= $article->pid ?>">
|
||||
<div class="content">
|
||||
<div class="actions">
|
||||
<div class="ts secondary buttons">
|
||||
<button class="ts icon like button" data-id="<?= $article->pid ?>">
|
||||
<i class="thumbs <?php if (!$article->is_like($user)) { echo "outline"; }?> up icon"></i> <?= $article->likes_count ?>
|
||||
</button>
|
||||
<a class="ts icon button" href="post.php?pid=<?= $article->pid ?>">
|
||||
Read <i class="right arrow icon"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header"><?= $article->title ?></div>
|
||||
<div class="middoted meta">
|
||||
<a href="user.php?username=<?= $article->author ?>"><?= $article->name ?></a>
|
||||
<span><?= date('Y-m-d', strtotime($article->time)) ?></span>
|
||||
</div>
|
||||
<div class="description" id="markdown-post-<?= $article->pid ?>">
|
||||
<div class="markdown">
|
||||
<?= sumarize($article->content, 5) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="secondary right aligned extra content">
|
||||
<i class="discussions icon"></i> <?= $article->comments_count ?> 則留言
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php echo cavern_pages(@$_GET['page'], $all_posts_count, $blog['limit']);
|
||||
} else {
|
||||
$view->show_message('inverted info', '沒有文章,趕快去新增一個吧!');
|
||||
}
|
||||
|
||||
$view->render();
|
||||
?>
|
118
login.php
Normal file
118
login.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
require_once('include/security.php');
|
||||
require_once('connection/SQL.php');
|
||||
require_once('config.php');
|
||||
|
||||
if (isset($_SESSION['cavern_username'])) {
|
||||
if (isset($_GET['logout'])) {
|
||||
if (validate_csrf()) {
|
||||
cavern_logout();
|
||||
header('axios-location: index.php?ok=logout');
|
||||
} else {
|
||||
http_response_code(403);
|
||||
echo json_encode(array("status" => 'csrf'));
|
||||
}
|
||||
} else if (isset($_GET['next']) && $_GET['next'] == "admin") {
|
||||
header("Location: ./admin/");
|
||||
} else {
|
||||
header('Location: index.php');
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ((isset($_POST['username'])) && (isset($_POST['password'])) && ($_POST['username']!='') && ($_POST['password']!='')) {
|
||||
if (cavern_login($_POST['username'], $_POST['password']) == 1) {
|
||||
if (isset($_POST['next']) && trim($_POST['next']) == "admin") {
|
||||
header('Location: ./admin/');
|
||||
} else if ((isset($_POST['next']) && filter_var($_POST['next'], FILTER_VALIDATE_URL)) || isset($_SERVER['HTTP_REFERER'])) {
|
||||
// redirect to previous page before login
|
||||
$next = (isset($_POST['next']) ? $_POST['next'] : $_SERVER['HTTP_REFERER']); // users login directly from navbar
|
||||
$url_data = parse_url($next);
|
||||
|
||||
$len = strlen("index.php");
|
||||
if (mb_substr($url_data['path'], -$len) === "index.php") {
|
||||
// the user was viewing the index page, so we just redirect him to index page
|
||||
header('Location: index.php?ok=login');
|
||||
} else {
|
||||
if (!isset($url_data['query'])) {
|
||||
$url_data['query'] = "ok=login";
|
||||
} else if (!strpos($url_data['query'], "ok=login")) {
|
||||
// for those already have url queries, such as 'post.php?pid=1'
|
||||
$url_data['query'] .= "&ok=login";
|
||||
}
|
||||
|
||||
$url = "{$url_data['path']}?{$url_data['query']}";
|
||||
header("Location: $url");
|
||||
}
|
||||
} else {
|
||||
// previous page doesn't exist, so we just redirect to default page
|
||||
header('Location: index.php?ok=login');
|
||||
}
|
||||
} else {
|
||||
header('Location: index.php?err=login');
|
||||
}
|
||||
exit;
|
||||
} else {
|
||||
$admin = (isset($_GET['next']) && trim($_GET['next']) == "admin");
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/2.3.3/tocas.css" rel='stylesheet'>
|
||||
<title>登入 | <?php echo $blog['name']; ?></title>
|
||||
<style type="text/css">
|
||||
html,body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
background: linear-gradient(180deg, deepskyblue 5%, aqua);
|
||||
}
|
||||
.ts.narrow.container {
|
||||
padding: 4em 0;
|
||||
}
|
||||
.segment {
|
||||
max-width: 300px;
|
||||
}
|
||||
/* admin style */
|
||||
body.admin {
|
||||
background: linear-gradient(0deg, #1CB5E0, #000046);
|
||||
}
|
||||
body.admin .ts.header, body.admin .ts.header .sub.header{
|
||||
color: white;
|
||||
}
|
||||
.inverted .ts.form .field > label {
|
||||
color: #EFEFEF;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body <?= ($admin ? 'class="admin"' : "") ?>>
|
||||
<div class="ts narrow container">
|
||||
<h1 class="ts center aligned header">
|
||||
<?= $blog['name'] ?>
|
||||
<div class="sub header"><?= ($admin ? "安全門" : "傳送門") ?></div>
|
||||
</h1>
|
||||
<div class="ts centered <?= ($admin ? "inverted" : "secondary") ?> segment">
|
||||
<form class="ts form" method="POST" action="login.php">
|
||||
<div class="field">
|
||||
<label>帳號</label>
|
||||
<input type="text" name="username">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>密碼</label>
|
||||
<input type="password" name="password">
|
||||
</div>
|
||||
<input type="hidden" name="next" value="<?= ($admin ? "admin" : @$_SERVER['HTTP_REFERER']); ?>">
|
||||
<div class="ts separated vertical fluid buttons">
|
||||
<input type="submit" class="ts positive button" value="登入">
|
||||
<a href="account.php?new" class="ts button">註冊</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php }
|
||||
?>
|
57
notification.php
Normal file
57
notification.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
require_once('connection/SQL.php');
|
||||
require_once('config.php');
|
||||
require_once('include/view.php');
|
||||
|
||||
if (isset($_SESSION['cavern_username'])) {
|
||||
$view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], "通知");
|
||||
$view->add_script_source("ts('.ts.dropdown:not(.basic)').dropdown();");
|
||||
$view->add_script("./include/js/security.js");
|
||||
|
||||
$notice_list = cavern_query_result("SELECT * FROM `notification` WHERE `username` = '%s' ORDER BY `time` DESC", array($_SESSION['cavern_username']));
|
||||
|
||||
if ($notice_list['num_rows'] > 0) {
|
||||
$regex = array(
|
||||
"username" => "/\{([^\{\}]+)\}@(\w+)/",
|
||||
"url" => "/\[([^\[\[]*)\]/"
|
||||
);
|
||||
?>
|
||||
<div class="ts big dividing header">通知 <span class="notification description">#僅顯示最近 100 則通知</span></div>
|
||||
<div class="table wrapper">
|
||||
<table class="ts sortable celled striped table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>內容</th>
|
||||
<th>日期</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
do {
|
||||
$message = $notice_list['row']['message'];
|
||||
$time = $notice_list['row']['time'];
|
||||
$url = $notice_list['row']['url'];
|
||||
|
||||
$message = preg_replace($regex["username"], '<a href="user.php?username=$2">$1</a>', $message);
|
||||
$message = preg_replace($regex["url"], "<a href=\"${url}\">$1</a>", $message);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $message ?></td>
|
||||
<td class="collapsing"><?= $time ?></td>
|
||||
</tr>
|
||||
<?php } while ($notice_list['row'] = $notice_list['query']->fetch_assoc()); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
} else {
|
||||
$view->show_message('inverted info', '目前沒有通知。');
|
||||
}
|
||||
|
||||
$view->render();
|
||||
} else {
|
||||
http_response_code(204);
|
||||
header('Location: index.php?err=nologin');
|
||||
exit;
|
||||
}
|
||||
?>
|
460
post.php
Normal file
460
post.php
Normal file
@ -0,0 +1,460 @@
|
||||
<?php
|
||||
require_once('connection/SQL.php');
|
||||
require_once('config.php');
|
||||
|
||||
set_include_path('include/');
|
||||
require_once('view.php');
|
||||
require_once('user.php');
|
||||
require_once('article.php');
|
||||
require_once('security.php');
|
||||
require_once('notification.php');
|
||||
|
||||
$user = validate_user();
|
||||
if (!$user->valid) {
|
||||
http_response_code(403);
|
||||
header("Location: ../index.php?err=account");
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($user->islogin && isset($_POST['pid']) && isset($_POST['title']) && isset($_POST['content'])) {
|
||||
if ($user->level < 1 || $user->muted == 1) {
|
||||
http_response_code(403);
|
||||
header('axios-location: post.php?err=level');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!validate_csrf()) {
|
||||
http_response_code(403);
|
||||
header('axios-location: post.php?err=level');
|
||||
}
|
||||
|
||||
if ($_POST['pid'] == "-1") {
|
||||
// new post
|
||||
if (trim($_POST['content']) == "") {
|
||||
http_response_code(400);
|
||||
header('axios-location: post.php?err=empty');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (trim($_POST['title']) == "") {
|
||||
$_POST['title'] = "(無標題)";
|
||||
}
|
||||
|
||||
$current = date('Y-m-d H:i:s');
|
||||
$SQL->query("INSERT INTO `post` (`title`, `content`, `time`, `username`) VALUES ('%s', '%s', '%s', '%s')", array(htmlspecialchars($_POST['title']), htmlspecialchars($_POST['content']), $current, $user->username));
|
||||
$pid = $SQL->insert_id();
|
||||
|
||||
// notify tagged user
|
||||
// the user who tag himself is unnecessary to notify
|
||||
$username_list = parse_user_tag($_POST['content']);
|
||||
foreach ($username_list as $key => $id) {
|
||||
if ($id == $user->username) continue;
|
||||
cavern_notify_user($id, "{{$user->name}}@{$user->username} 在 [{$_POST['title']}] 中提到了你", "post.php?pid=$pid");
|
||||
}
|
||||
|
||||
http_response_code(201); // 201 Created
|
||||
header('axios-location: post.php?pid='.$pid);
|
||||
exit;
|
||||
} else {
|
||||
// edit old post
|
||||
$pid = abs($_POST['pid']);
|
||||
|
||||
try {
|
||||
$post = new Article($pid);
|
||||
} catch (NoPostException $e) {
|
||||
// post not found
|
||||
http_response_code(404);
|
||||
header("axios-location: index.php?err=post");
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($post->author !== $user->username && $user->level < 8) {
|
||||
http_response_code(403);
|
||||
header('axios-location: post.php?err=edit');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (trim($_POST['content']) == "") {
|
||||
http_response_code(400);
|
||||
header('axios-location: post.php?err=empty');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (trim($_POST['title']) == "") {
|
||||
$_POST['title'] = "(無標題)";
|
||||
}
|
||||
|
||||
$post->modify($user, "title", htmlspecialchars($_POST['title']));
|
||||
$post->modify($user, "content", htmlspecialchars($_POST['content']));
|
||||
|
||||
$post->save();
|
||||
header('axios-location: post.php?pid='.$_POST['pid']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->islogin && isset($_GET['del']) && trim($_GET['del']) != '') {
|
||||
if (!validate_csrf()) {
|
||||
http_response_code(403);
|
||||
header('axios-location: post.php?err=level');
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$post = new Article(intval($_GET['del']));
|
||||
} catch (NoPostException $e) {
|
||||
http_response_code(404);
|
||||
header('axios-location: index.php?err=post');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($post->author !== $user->username && $user->level < 8) {
|
||||
http_response_code(403);
|
||||
header('axios-location: post.php?err=del');
|
||||
exit;
|
||||
} else {
|
||||
$SQL->query("DELETE FROM `post` WHERE `pid`='%d' AND `username` = '%s'", array($_GET['del'], $_SESSION['cavern_username']));
|
||||
http_response_code(204);
|
||||
header('axios-location: post.php?ok=del');
|
||||
exit;
|
||||
}
|
||||
} else if (!$user->islogin && isset($_GET['del'])) {
|
||||
http_response_code(204);
|
||||
header('axios-location: index.php?err=nologin');
|
||||
exit;
|
||||
}
|
||||
|
||||
// View
|
||||
if (isset($_GET['pid'])) {
|
||||
$pid = abs($_GET['pid']);
|
||||
|
||||
try {
|
||||
$post = new Article($pid);
|
||||
} catch (NoPostException $e) {
|
||||
http_response_code(404);
|
||||
header('Location: index.php?err=post');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($user->islogin) {
|
||||
$view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], $post->title);
|
||||
$view->add_script_source("ts('.ts.dropdown:not(.basic)').dropdown();");
|
||||
$owner_view = ($post->author === $user->username);
|
||||
} else {
|
||||
$view = new View('theme/default.html', 'theme/nav/default.html', 'theme/sidebar.php', $blog['name'], $post->title);
|
||||
$owner_view = FALSE;
|
||||
}
|
||||
|
||||
$view->add_script("https://unpkg.com/load-js@1.2.0");
|
||||
$view->add_script("./include/js/lib/editormd.js");
|
||||
$view->add_script("./include/js/lib/css.min.js");
|
||||
$view->add_script("./include/js/security.js");
|
||||
$view->add_script("./include/js/markdown.js");
|
||||
$view->add_script("./include/js/comment.js");
|
||||
$view->add_script("./include/js/post.js");
|
||||
$view->add_script("./include/js/like.js");
|
||||
$view->add_script_source("ts('.ts.tabbed.menu .item').tab();");
|
||||
|
||||
if (isset($_GET['ok'])) {
|
||||
if ($_GET['ok'] == "login" && $user->islogin) {
|
||||
$greeting = cavern_greeting();
|
||||
$view->show_message("inverted positive", "{$greeting}!我的朋友!");
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="ts<?php echo ($owner_view ? " stackable " : " "); ?>grid">
|
||||
<div class="stretched column" id="header">
|
||||
<h2 class="ts header">
|
||||
<?= $post->title ?>
|
||||
<div class="sub header"><a href="user.php?username=<?= $post->author ?>"><?= $post->name ?></a></div>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="action column">
|
||||
<div class="ts secondary icon buttons">
|
||||
<button class="ts secondary icon like button" data-id="<?= $pid ?>">
|
||||
<i class="thumbs <?php if (!$post->is_like($user)) {echo "outline";}?> up icon"></i> <?= $post->likes_count ?>
|
||||
</button>
|
||||
<?php
|
||||
if ($owner_view) { ?>
|
||||
<a class="ts secondary icon button" href="post.php?edit=<?= $pid ?>">
|
||||
<i class="edit icon"></i>
|
||||
</a>
|
||||
<a class="ts secondary icon delete button" href="post.php?del=<?= $pid ?>">
|
||||
<i class="trash icon"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts segments">
|
||||
<div class="ts flatted segment" id="post" data-id="<?= $pid ?>">
|
||||
<div class="markdown"><?= $post->content ?></div>
|
||||
</div>
|
||||
<div class="ts right aligned tertiary segment">
|
||||
<i class="clock icon"></i><?= $post->time ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment header">
|
||||
<div class="ts grid">
|
||||
<div class="stretched header column">
|
||||
<div class="ts big header">留言</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<span class="fetch time">Last fetch: --:--</span>
|
||||
<div class="ts active inline loader"></div>
|
||||
<button class="ts fetch icon button">
|
||||
<i class="refresh icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts comment divider"></div>
|
||||
</div>
|
||||
<div class="ts comments">
|
||||
<div class="ts borderless flatted no-comment segment">現在還沒有留言!</div>
|
||||
</div>
|
||||
<div class="ts segments" id="comment">
|
||||
<div class="ts fitted secondary segment">
|
||||
<div class="ts tabbed menu">
|
||||
<a class="active item" data-tab="textarea">Write</a>
|
||||
<a class="item" data-tab="preview">Preview</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts clearing active tab segment" data-tab="textarea">
|
||||
<?php if ($user->islogin) {
|
||||
if ($user->muted) {
|
||||
$disabled = " disabled";
|
||||
$placeholder = "你被禁言了。";
|
||||
$button_text = "你被禁言了";
|
||||
} else {
|
||||
$disabled = "";
|
||||
$placeholder = "留言,然後開戰。";
|
||||
$button_text = "留言";
|
||||
}
|
||||
} else {
|
||||
$disabled = " disabled";
|
||||
$placeholder = "請先登入";
|
||||
$button_text = "留言";
|
||||
} ?>
|
||||
<div class="ts<?= $disabled ?> fluid input">
|
||||
<textarea placeholder="<?= $placeholder ?>" rows="5" autocomplete="off"<?= $disabled ?>></textarea>
|
||||
</div>
|
||||
<div class="ts<?= $disabled ?> right floated separated action buttons">
|
||||
<button class="ts positive submit button"><?= $button_text ?></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts tab segment" id="preview" data-tab="preview"></div>
|
||||
</div>
|
||||
<?php $view->render();
|
||||
|
||||
} else if (isset($_GET['new']) || isset($_GET['edit'])) {
|
||||
// New or Edit
|
||||
if (!$user->islogin) {
|
||||
header('Location: index.php?err=nologin');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($user->level < 1 || $user->muted) {
|
||||
header('Location: post.php?err=level');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['new'])) {
|
||||
$mode = "new";
|
||||
|
||||
$pid = -1;
|
||||
$title = "";
|
||||
$content = "";
|
||||
} else if (isset($_GET['edit'])) {
|
||||
$mode = "edit";
|
||||
|
||||
$pid = abs($_GET['edit']);
|
||||
try {
|
||||
$post = new Article($pid);
|
||||
} catch (NoPostException $e) {
|
||||
http_response_code(404);
|
||||
header('Location: index.php?err=post');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($post->author != $user->username) {
|
||||
http_response_code(403);
|
||||
header('Location: post.php?err=edit');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$title = $post->title;
|
||||
$content = $post->content;
|
||||
|
||||
$view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], ($title == "" ? "文章" : $title));
|
||||
|
||||
$view->add_script_source("ts('.ts.dropdown:not(.basic)').dropdown();");
|
||||
$view->add_script("./include/js/lib/editormd.js");
|
||||
$view->add_script("./include/js/lib/zh-tw.js");
|
||||
$view->add_script("./include/js/lib/css.min.js");
|
||||
$view->add_script("./include/js/security.js");
|
||||
$view->add_script("./include/js/edit.js");
|
||||
?>
|
||||
<form action="post.php" method="POST" name="edit" id="edit" autocomplete="off"> <!-- prevent Firefox from autocompleting -->
|
||||
<div class="ts stackable grid">
|
||||
<div class="stretched column">
|
||||
<div class="ts huge fluid underlined input">
|
||||
<input placeholder="標題" name="title" value="<?= $title ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="action column">
|
||||
<div class="ts buttons">
|
||||
<button class="ts positive button">發布</button>
|
||||
<?php if ($mode == "edit") { ?>
|
||||
<a href="post.php?del=<?= $pid ?>" class="ts negative delete button">刪除</a>
|
||||
<?php } ?>
|
||||
<a href="index.php" class="ts button">取消</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="markdownEditor">
|
||||
<textarea id="markdown" name="content" stlye="display: none;" autocomplete="off" autocorrect="off" spellcheck="false"><?= $content ?></textarea>
|
||||
</div>
|
||||
<input type="hidden" name="pid" id="pid" value="<?= $pid ?>">
|
||||
</form>
|
||||
<?php $view->render();
|
||||
|
||||
} else {
|
||||
// List all
|
||||
if (!$user->islogin && (!isset($_GET['username']) || trim($_GET['username']) == "")) {
|
||||
http_response_code(403);
|
||||
header('Location: index.php?err=nologin');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['username']) && trim($_GET['username']) != "") {
|
||||
$username = trim($_GET['username']);
|
||||
|
||||
try {
|
||||
$target_user = new User($username);
|
||||
} catch (NoUserException $e) {
|
||||
http_response_code(404);
|
||||
header('Location: user.php?err=no');
|
||||
exit;
|
||||
}
|
||||
|
||||
$post_list = article_list(cavern_query_result(
|
||||
"SELECT * FROM `post` WHERE `username`='%s' ORDER BY `time`",
|
||||
array($username))
|
||||
);
|
||||
} else if ($user->islogin) {
|
||||
$username = $user->username;
|
||||
$post_list = article_list(cavern_query_result(
|
||||
"SELECT * FROM `post` WHERE `username`='%s' ORDER BY `time`",
|
||||
array($username))
|
||||
);
|
||||
}
|
||||
|
||||
$owner_view = ($user->islogin && $username === $user->username);
|
||||
|
||||
if ($user->islogin) {
|
||||
$view = new View('theme/default.html','theme/nav/util.php', 'theme/sidebar.php', $blog['name'], "文章");
|
||||
$view->add_script_source("$('tbody').on('click', 'a.negative.button', function(e) {
|
||||
e.preventDefault();
|
||||
let el = e.currentTarget;
|
||||
let href = el.getAttribute('href');
|
||||
swal({
|
||||
type: 'question',
|
||||
title: '確定要刪除嗎?',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '確定',
|
||||
cancelButtonText: '取消',
|
||||
}).then((result) => {
|
||||
if (result.value) { // confirm
|
||||
axios.request({
|
||||
method: 'GET',
|
||||
maxRedirects: 0,
|
||||
url: href
|
||||
}).then(function (res) {
|
||||
location.href = res.headers['axios-location'];
|
||||
});
|
||||
}
|
||||
});
|
||||
});");
|
||||
} else {
|
||||
$view = new View('theme/default.html','theme/nav/default.html', 'theme/sidebar.php', $blog['name'], "文章");
|
||||
}
|
||||
|
||||
$view->add_script("./include/js/security.js");
|
||||
$view->add_script_source("ts('.ts.dropdown').dropdown();\nts('.ts.sortable.table').tablesort();");
|
||||
|
||||
if (isset($_GET['ok'])) {
|
||||
if ($_GET['ok'] == "del") {
|
||||
$view->show_message("inverted positive", "刪除成功");
|
||||
} else if ($_GET['ok'] == "login" && $user->islogin) {
|
||||
$greeting = cavern_greeting();
|
||||
$view->show_message("inverted positive", "{$greeting}!我的朋友!");
|
||||
}
|
||||
}
|
||||
if (isset($_GET['err'])) {
|
||||
switch ($_GET['err']) {
|
||||
case 'del':
|
||||
$view->show_message("inverted negative", "刪除失敗");
|
||||
break;
|
||||
case 'edit':
|
||||
$view->show_message("inverted negative", "編輯失敗");
|
||||
break;
|
||||
case 'empty':
|
||||
$view->show_message("warning", "文章內容不能為空!");
|
||||
break;
|
||||
case 'level':
|
||||
$view->show_message("inverted negative", "你沒有權限發文!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="ts big dividing header">
|
||||
文章
|
||||
<?php if (!$owner_view) { // List other one's post ?>
|
||||
<div class="sub header"><a href="user.php?username=<?= $username ?>"><?= $target_user->name ?></a></div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="table wrapper">
|
||||
<table class="ts sortable celled striped table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>標題</th>
|
||||
<th>讚</th>
|
||||
<th>留言</th>
|
||||
<th>日期</th>
|
||||
<?php if ($owner_view) { // Only owner could manage post ?>
|
||||
<th>管理</th>
|
||||
<?php } ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
if (sizeof($post_list) > 0) {
|
||||
foreach ($post_list as $key => $article) {
|
||||
?>
|
||||
<tr>
|
||||
<td><a href="post.php?pid=<?= $article->pid ?>"><?= $article->title ?></a></td>
|
||||
<td class="center aligned collapsing"><?= $article->likes_count ?></td>
|
||||
<td class="center aligned collapsing"><?= $article->comments_count ?></td>
|
||||
<td class="collapsing"><?= $article->time ?></td>
|
||||
<?php if ($owner_view) { // Only owner could manage post ?>
|
||||
<td class="right aligned collapsing">
|
||||
<a class="ts circular icon button" href="post.php?edit=<?= $article->pid ?>"><i class="pencil icon"></i></a>
|
||||
<a class="ts negative circular icon button" href="post.php?del=<?= $article->pid ?>"><i class="trash icon"></i></a>
|
||||
</td>
|
||||
<?php } ?>
|
||||
</tr>
|
||||
<?php }
|
||||
} else { ?>
|
||||
<tr>
|
||||
<td colspan="<?php echo ($owner_view ? 5 : 4); ?>">沒有文章</td>
|
||||
<tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
$view->render();
|
||||
}
|
||||
?>
|
51
theme/default.html
Normal file
51
theme/default.html
Normal file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{part} | {title}</title>
|
||||
<!-- Tocas UI:CSS 與元件 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/2.3.3/tocas.css">
|
||||
<!-- Tocas JS:模塊與 JavaScript 函式 -->
|
||||
<script src="include/js/lib/tocas.js"></script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@7.26.11/dist/sweetalert2.all.min.js"></script>
|
||||
<link rel="stylesheet" href="https://pandao.github.io/editor.md/css/editormd.css" />
|
||||
<link rel="stylesheet" href="include/css/cavern.css">
|
||||
</head>
|
||||
<body>
|
||||
{nav}
|
||||
<div class="ts large vertically padded fluid heading slate">
|
||||
<div class="ts narrow container">
|
||||
<div class="header">{title}</div>
|
||||
<div class="description">Welcome to {title}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts narrow container" id="main">
|
||||
<div class="ts stackable grid">
|
||||
<div class="twelve wide column" id="content">
|
||||
{message}
|
||||
{content}
|
||||
</div>
|
||||
<div class="four wide column" id="sidebar">
|
||||
{sidebar}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<div class="ts divider"></div>
|
||||
<div class="ts center aligned basic segment">
|
||||
Powered by <a href="https://app.stoneapp.tech/#cavern">Cavern</a>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Anchor -->
|
||||
<div class="ts bottom right snackbar">
|
||||
<div class="content"></div>
|
||||
<a class="action"></a>
|
||||
</div>
|
||||
<!-- Scripts -->
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
{script}
|
||||
</body>
|
||||
</html>
|
16
theme/nav/default.html
Normal file
16
theme/nav/default.html
Normal file
@ -0,0 +1,16 @@
|
||||
<div class="ts top attached pointing secondary large menu" id="menu">
|
||||
<div class="ts narrow container">
|
||||
<a href="index.php" class="active item">首頁</a>
|
||||
<form class="right fitted item" action="login.php" method="POST" name="login">
|
||||
<div class="tablet or large device only">
|
||||
<div class="ts small underlined input">
|
||||
<input placeholder="Username" type="text" name="username">
|
||||
<input placeholder="Password" type="password" name="password">
|
||||
</div>
|
||||
</div>
|
||||
<button class="ts small primary login icon button">
|
||||
<i class="sign in icon"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
28
theme/nav/util.php
Normal file
28
theme/nav/util.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
$self = @end(explode('/',$_SERVER['PHP_SELF']));
|
||||
?>
|
||||
<div class="ts top attached pointing secondary large menu" id="menu">
|
||||
<div class="ts narrow container">
|
||||
<a href="index.php" class="<?php if ($self == 'index.php') { echo "active "; } ?>item">首頁</a>
|
||||
<div class="ts <?php if ($self == 'post.php') { echo "active "; } ?>dropdown item">
|
||||
<div class="text">文章</div>
|
||||
<div class="menu">
|
||||
<a href="post.php?new" class="item">新增</a>
|
||||
<a href="post.php" class="item">列表</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="account.php" class="<?php if ($self == 'account.php') { echo "active "; } ?>item">帳號</a>
|
||||
<div class="right menu">
|
||||
<a href="#" class="notification icon item"><i class="bell outline icon"></i></a>
|
||||
<a href="#" class="item" id="logout">登出</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts narrow container" id="notification-wrapper">
|
||||
<div class="notification container">
|
||||
<div class="ts borderless top attached segment">通知</div>
|
||||
<div class="ts relaxed divided feed"></div>
|
||||
<a class="ts bottom attached fluid button" href="notification.php">看所有通知</a>
|
||||
</div>
|
||||
</div>
|
||||
<script src="include/js/notification.js"></script>
|
40
theme/sidebar.php
Normal file
40
theme/sidebar.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
require_once('connection/SQL.php');
|
||||
require_once('include/user.php');
|
||||
require_once('config.php');
|
||||
|
||||
$user = validate_user();
|
||||
|
||||
if(!$user->islogin){ ?>
|
||||
<div class="ts basic center aligned padded segment">
|
||||
登入或是<a href="account.php?new">註冊</a>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<a class="ts center aligned big header" data-username="<?= $user->username ?>" href="user.php?username=<?= $user->username ?>">
|
||||
<img class="ts circular avatar image" src="https://www.gravatar.com/avatar/<?= md5(strtolower($user->email)) ?>?d=https%3A%2F%2Ftocas-ui.com%2Fassets%2Fimg%2F5e5e3a6.png&s=150"> <?= $user->name ?>
|
||||
<?php if ($user->muted) { ?>
|
||||
<div class="negative sub header">
|
||||
<i class="ban icon"></i>你已被禁言!
|
||||
</div>
|
||||
<?php } ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
|
||||
<div class="ts fluid icon input">
|
||||
<input type="text" placeholder="在這搜尋人、事、物">
|
||||
<i class="inverted circular search link icon"></i>
|
||||
</div>
|
||||
<!-- Segment 1 -->
|
||||
<div class="ts tertiary top attached center aligned segment">名稱</div>
|
||||
<div class="ts bottom attached segment">
|
||||
<p>項目</p>
|
||||
<p>項目</p>
|
||||
<p>項目</p>
|
||||
</div>
|
||||
<!-- Segment 2 -->
|
||||
<div class="ts tertiary top attached center aligned segment">名稱</div>
|
||||
<div class="ts bottom attached segment">
|
||||
<p>項目</p>
|
||||
<p>項目</p>
|
||||
<p>項目</p>
|
||||
</div>
|
BIN
theme/thinking.png
Normal file
BIN
theme/thinking.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
108
user.php
Normal file
108
user.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
require_once('connection/SQL.php');
|
||||
require_once('config.php');
|
||||
require_once('include/view.php');
|
||||
|
||||
if (isset($_GET['username']) && trim($_GET['username']) != "") {
|
||||
$username = trim($_GET['username']);
|
||||
$result = cavern_query_result("SELECT * FROM `user` WHERE `username`='%s'", array($username));
|
||||
if ($result['num_rows'] > 0) {
|
||||
$name = $result['row']['name'];
|
||||
$level = $result['row']['level'];
|
||||
$email = md5(strtolower($result['row']['email']));
|
||||
$role = cavern_level_to_role($level);
|
||||
$posts = cavern_query_result("SELECT * FROM `post` WHERE `username`='%s'", array($username));
|
||||
$posts_count = ($posts['num_rows'] > 0 ? $posts['num_rows'] : 0);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
header('Location: user.php?err=no');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_SESSION['cavern_username'])) {
|
||||
$view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], $name);
|
||||
$view->add_script_source("ts('.ts.dropdown').dropdown();");
|
||||
} else {
|
||||
$view = new View('theme/default.html', 'theme/nav/default.html', 'theme/sidebar.php', $blog['name'], $name);
|
||||
}
|
||||
$view->add_script("./include/js/security.js");
|
||||
|
||||
if (isset($_GET['err'])) {
|
||||
if ($_GET['err'] == "no") {
|
||||
$view->show_message('negative', "找不到使用者");
|
||||
$view->render();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="ts big dividing header"><?= $name ?> 的個人資料</div>
|
||||
<div class="ts stackable grid">
|
||||
<div class="column">
|
||||
<div class="ts center aligned flatted borderless segment">
|
||||
<img src="https://www.gravatar.com/avatar/<?= $email ?>?d=https%3A%2F%2Ftocas-ui.com%2Fassets%2Fimg%2F5e5e3a6.png&s=500" class="ts rounded image" id="avatar">
|
||||
</div>
|
||||
</div>
|
||||
<div class="stretched column">
|
||||
<div class="table wrapper">
|
||||
<table class="ts borderless three column table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">基本資料</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>使用者名稱</td>
|
||||
<td><?= $username ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>暱稱</td>
|
||||
<td><?= $name ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>權限</td>
|
||||
<td><?= $role ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="ts borderless two column table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">統計</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>文章數</td>
|
||||
<td><?= $posts_count ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="right aligned"><a href="post.php?username=<?= $username ?>">看他的文章 <i class="hand outline right icon"></i></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $view->render();
|
||||
} else {
|
||||
if (isset($_GET['err'])) {
|
||||
if (isset($_SESSION['cavern_username'])) {
|
||||
$view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], "使用者");
|
||||
$view->add_script_source("ts('.ts.dropdown').dropdown();");
|
||||
} else {
|
||||
$view = new View('theme/default.html', 'theme/nav/default.html', 'theme/sidebar.php', $blog['name'], "使用者");
|
||||
}
|
||||
$view->add_script("./include/js/security.js");
|
||||
|
||||
if ($_GET['err'] == "no") {
|
||||
$view->show_message('negative', "找不到使用者");
|
||||
$view->render();
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
header('Location: user.php?err=no');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
Loading…
x
Reference in New Issue
Block a user