flush -- WIP admin panel

This commit is contained in:
Tony Yang
2019-05-30 14:18:07 +08:00
parent 75af7df7b4
commit 84fb4180c9
53 changed files with 9104 additions and 2 deletions

80
include/article.php Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}
};