Compare commits

...

19 Commits

Author SHA1 Message Date
e538a76b09 Начат запил страницы отображающей пост 2024-04-27 20:47:41 +03:00
62b7b68976 Функция поиска по постам
Мне это всё расписывать что-ли? Смотрите в содержание коммита, мне феерически индифферентно
2024-03-07 19:58:39 +03:00
8700a544b9 Удаление config.json
Для примера конфигурационного файла - см. config.json.example
2024-02-09 02:53:41 +05:30
705e8cd6a2 Создание поста
Метод API для создания поста; страница фронта и стили для него; новый тип ошибки при неудачной загрузке файла; фикс функции проверки строки на соответствие кодировке ASCII; фикс парсинга тегов; умное создание превью (проверка на случай, если превью получилось больше оригинала); исправление функции сохранения изображения; фикс функции создания поста, которая взаимодействует с БД; добавлена проверка корректности подписи к посту; добавление новых пунктов в навигацию; небольшое улучшение QoL в плане конфига.
2024-02-09 00:13:23 +03:00
de456dea0a Страница со статистикой
Сделана основа для страницы со статистикой и несколько функций в бэке для неё
2024-02-01 13:20:39 +03:00
751476c4f8 Страница с одобренными тегами 2024-01-27 01:43:27 +03:00
93a2286d46 Метод API для одобренных тегов
Запрос одобренных тегов и проверка конкретного на одобренность, частичные правки мусорных варнов в логах, полунерабочая функция проверки строки на соответствие ASCII, проверки ввода в методе создания поста, мелкие оптимизации запросов к БД в методе запроса данных юзера.
2024-01-20 20:05:45 +03:00
e180c04e44 Продолжение запила API
Создание постов, новые настройки в конфиге
2024-01-20 00:02:51 +03:00
c48f837738 Регистрация
Фронт (стили+страница), полировка стр. логина, микроправки и микрооптимизации
2024-01-15 04:58:29 +03:00
4e1c36d670 Реструктура стилей, страница логина, уведомления 2024-01-13 03:24:42 +03:00
6958b75414 Редизайн главной страницы 2023-12-22 06:10:49 +03:00
8d74a51937 Проект ещё жив! 2023-12-20 06:08:13 +03:00
a573faf5a1 Продолжение переписывания 2023-11-03 02:38:51 +03:00
12143c148d Продолжение разработки 23.10.31
Добавлен .gitignore, скрыты несколько нинужных файлов, в целом продолжен запил основных частей функционала, начат микрорефакторинг (теперь концентрация индусского кода будет чуть меньше).
2023-10-31 21:57:17 +03:00
e487ed79c4 Добавил проверку изображения в апи постов и ещё по мелочи 2023-09-07 23:05:23 +03:00
9a4658f3ea Начал делать API для постов 2023-09-06 05:38:18 +03:00
983a5d0353 Наконец-то разобрался с сессиями, +минорные фиксы 2023-08-31 23:26:16 +03:00
ae719995ee api/user/__admin_session.php and random tweaks 2023-08-30 05:16:15 +03:00
074ce120e9 Code cleanup and some fixes 2023-08-30 04:41:13 +03:00
56 changed files with 3610 additions and 366 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
_temp/
front/styles/bg_pattern_peace_old.png
test.png
TODO.md
front/images/*
front/images/counter/*
config.json

View File

@@ -5,8 +5,12 @@
The newest generation imageboard.
## Remarks
<!--
sudo rm -r /usr/share/nginx/html/testing/E949 && sudo cp -R . /usr/share/nginx/html/testing/E949/ && sudo chown -R http:http /usr/share/nginx/html/testing
- `sudo rm -r /usr/share/nginx/html/testing/E949 && sudo cp -R . /usr/share/nginx/html/testing/E949/ && sudo chown -R http:http /usr/share/nginx/html/testing`
mysql -u e949 -p
- `#049e59` -> `#094e59`
#049e59 -> #094e59
php requires extensions: mysqli gd intl
-->

19
TODO.md
View File

@@ -5,7 +5,7 @@
- Детальная стата по инстансу
- Демонстрация наполнения и управление БД
- "Большая Красная Кнопка"
- Общая статистика по инстансу
- ~~Общая статистика по инстансу~~
- Главная страница
- Страница регистрации
- Страница с отображением поста-картинки
@@ -20,9 +20,8 @@
- Айди приглашения
- Роли
- Новичок
- Может оценивать посты, но не более n в день
- Может создавать посты, но не более n в день и только с одобренными тегами
- Может удалять свои посты
- Может оценивать посты
- Не может создавать посты
- Может устанавливать себе аватарку
- Проверенный
- Нет лимитов на оценку постов
@@ -39,6 +38,7 @@
- Может банить кого угодно
- Может удалять чьи угодно аккаунты
- Может редактировать чужие посты
- Может управлять одобренными тегами
- Аватарки
- Бан
- Полное удаление
@@ -48,10 +48,10 @@
- Приглашения
- Регистрация по приглашению автоматически даёт роль "проверенный"
- Пост с картинкой
- Рекодирование пикчи в низкое разрешение для превью
- Описание
- Теги
- Добавление нового
- ~~Рекодирование пикчи в низкое разрешение для превью~~
- ~~Описание~~
- ~~Теги~~
- ~~Добавление нового~~
- Редактирование тегов существующего
- Удаление
- Оценки
@@ -59,7 +59,8 @@
- Статистика по всем картинкам
- Комментарии
- Теги
- Перечень одобренных
- ~~Перечень одобренных~~
- Добавление, редактирование и удаление одобренных
- Шаблонная разметка
- Локализация
- Кастомизация внешнего вида

View File

@@ -1,38 +1,62 @@
<?php
require_once("_db.php"); //("api/_db.php");
// Things related to authentication
//session_start();
// This ^ should be placed at login stage
// Includes
if (isset($IS_FRONTEND) && $IS_FRONTEND)
require_once("api/_db.php");
else
require_once("_db.php");
$LOGGED_IN = false;
if (isset($_SESSION["userid"])) {
// Check if user still exist
$s = $db->prepare("SELECT * FROM users WHERE id = ?");
$s->bind_param("s", $_SESSION["userid"]);
$s->execute();
if (!(bool)$s->get_result()->fetch_assoc()) { // If not, then destroy session
session_unset();
session_destroy();
echo "user id does not exist";
die("user id used in session does not exist");
}
$LOGGED_IN = true;
} else {
// ATTENTION: idk will this work, but this can be theoretically unsafe or cause fault
if (session_status()) {
session_unset();
session_destroy();
}
// End currently active session
function AUTH_EndSession () {
session_unset();
session_destroy();
if (isset($_COOKIE["PHPSESSID"])) {
unset($_COOKIE["PHPSESSID"]);
setcookie("PHPSESSID", "", time() - 3600, "/");
}
}
// A few tips:
// session_start() - start OR RESUME session
// If $_SESSION["userid"] is set - it counted as active login session
// If its not set - it counted as no login session
session_start();
$LOGGED_IN = false;
$THIS_USER = null; // ID of logged in user
if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION["userid"])) { // If there are active session
// Check if user still exist
$s = $db->prepare("SELECT id FROM users WHERE id = ?");
$s->bind_param("s", $_SESSION["userid"]);
$s->execute();
if (!(bool)$s->get_result()->fetch_assoc()) { // If not, then destroy session
AUTH_EndSession();
die("user id used in session does not exist");
}
$LOGGED_IN = true;
$THIS_USER = $_SESSION["userid"];
} elseif (session_status() === PHP_SESSION_DISABLED) { // If sessions are disabled
die("ERROR: please enable sessions in php config");
}
// HACK
if ($Config["debug"] && isset($_REQUEST["debug"])) { // If there are not any session and debug mode is on
// ATTENTION: FOR DEBUG PURPOSES ONLY!
if ($_REQUEST["debug"] == "drop") {
AUTH_EndSession();
die("session discarded");
}
$_SESSION["userid"] = intval($_REQUEST["debug"]);
print_r(["created_session" => $_SESSION]);
die();
}
?>

View File

@@ -1,8 +1,11 @@
<?php // Parsing configuration file
<?php
// Parsing configuration file
$Config = array();
$Config_FileName = "config.json";
$Config_PossiblePaths = array(
$Config_PossiblePaths = array( // TODO: remake with flag $IS_FRONTEND
"./" . $Config_FileName,
"../" . $Config_FileName,
"../../" . $Config_FileName,
@@ -22,4 +25,22 @@ if (!$Config) {
die("invalid configuration file");
}
?>
// Checking paths on existence
function CreateDirIfNotExist ($path) {
if (!is_dir($path))
mkdir($path, 0755, true);
}
// Creating dirs at correct path
if (isset($IS_FRONTEND) && $IS_FRONTEND) {
CreateDirIfNotExist($Config["media"]["pics_path"]);
CreateDirIfNotExist($Config["media"]["prevs_path"]);
} else {
CreateDirIfNotExist("../" . $Config["media"]["pics_path"]);
CreateDirIfNotExist("../" . $Config["media"]["prevs_path"]);
}
?>

View File

@@ -1,6 +1,13 @@
<?php // Database setup
<?php
// Database setup
require_once("_config.php");
// Includes
if (isset($IS_FRONTEND) && $IS_FRONTEND)
require_once("api/_config.php");
else
require_once("_config.php");

View File

@@ -1,15 +0,0 @@
<?php
// Internal errors
$Err_Int_JSONEncode = "int.jsonencode"; // Failed to encode JSON data
// Request data parsing errors
$Err_RDP_InvalidID = "rdp.invalidid"; // Requested ID of resource is invalid
$Err_RDP_InvalidArgs = "rdp.invalidargs"; // Invalid arguments supplied to method
// Data processing errors
$Err_DP_IDNotFound = "dp.idnotfound"; // Resource not found by requested ID
$Err_DP_AlreadyLoggedIn = "dp.alreadyloggedin"; // User already logged into account
$Err_DP_RegClosed = "dp.regclosed"; // Registration is closed
$Err_DP_NotEnoughRole = "dp.notenoughrole"
?>

111
api/_errorslist.php Normal file
View File

@@ -0,0 +1,111 @@
<?php
// All existing errors
// All existing error codes as integers
const E_NOERROR = 0; // No error
// Unknown (unspecific) errors
const E_UNS_UNEXPECTED = 101; // Unexpected result
const E_UNS_NOTFOUND = 102; // Object not found
const E_UNS_INTERNAL = 103; // Internal error occured
const E_UNS_JSONBADINP = 104; // Cant encode object to JSON string
const E_UNS_NOTIMPL = 105; // Not yet implemented
// User input errors
const E_UIN_WRONGID = 201; // Wrong object id (not found)
const E_UIN_WRONGPATH = 202; // Wrong object path (not found)
const E_UIN_FILE2LARGE = 203; // File size is too large
const E_UIN_FILETYPE = 204; // Wrong file type
const E_UIN_IMGBADRES = 205; // Invalid image resolution
const E_UIN_INSUFARGS = 206; // Not enough arguments was supplied to method
const E_UIN_BADARGS = 207; // Bad arguments
const E_UIN_FAIL2UPLD = 208; // Failed to upload file
// Authentication errors
const E_AUT_ALRLOGIN = 301; // User is already logged in
const E_AUT_REGCLOSED = 302; // Registrations are closed
const E_AUT_PWD2WEAK = 303; // Password is too weak
const E_AUT_NOTAUTHED = 304; // Not authenticated
const E_AUT_WRONGCREDS = 305; // User with that credentials does not exist
// Access errors
const E_ACS_PERMDENIED = 401; // Permission to object denied
const E_ACS_INSUFROLE = 402; // Insufficient role
// Database-related errors
const E_DBE_UNKNOWN = 500; // Unknown error
const E_DBE_INSERTFAIL = 501; // INSERT query failed
const E_DBE_SELECTFAIL = 502; // SELECT query failed
const E_DBE_DELETEFAIL = 503; // DELETE query failed
// All existing errors as two-dimensional array
$Errors_Enum = array(
array("noerror", E_NOERROR, "no error"),
// Unspecific errors
array("uns.unexpected", E_UNS_UNEXPECTED, "unexpected result"),
array("uns.notfound", E_UNS_NOTFOUND, "object not found"),
array("uns.internal", E_UNS_INTERNAL, "internal error occured"),
array("uns.jsonbadinp", E_UNS_JSONBADINP, "cant encode object to json string"),
array("uns.notimpl", E_UNS_NOTIMPL, "not yet implemented"),
// User input errors
array("uin.wrongid", E_UIN_WRONGID, "wrong object id (not found)"),
array("uin.wrongpath", E_UIN_WRONGPATH, "wrong object path (not found)"),
array("uin.file2large", E_UIN_FILE2LARGE, "file size is too large"),
array("uin.filetype", E_UIN_FILETYPE, "wrong file type"),
array("uin.imgbadres", E_UIN_IMGBADRES, "invalid image resolution"),
array("uin.insufargs", E_UIN_INSUFARGS, "not enough arguments was supplied to method"),
array("uin.badargs", E_UIN_BADARGS, "bad arguments"),
array("uin.fail2upld", E_UIN_FAIL2UPLD, "failed to upload file"),
// Authentication errors
array("aut.alrlogin", E_AUT_ALRLOGIN, "already logged in"),
array("aut.regclosed", E_AUT_REGCLOSED, "registrations are closed"),
array("aut.pwd2weak", E_AUT_PWD2WEAK, "password is too weak"),
array("aut.notauthed", E_AUT_NOTAUTHED, "not authenticated"),
array("aut.wrongcreds", E_AUT_WRONGCREDS, "no such user name and/or password"),
// Access errors
array("acs.permdenied", E_ACS_PERMDENIED, "permission denied"),
array("acs.insufrole", E_ACS_INSUFROLE, "insufficient role"),
// Database-related errors
array("dbe.unknown", E_DBE_UNKNOWN, "unknown database error"),
array("dbe.insertfail", E_DBE_INSERTFAIL, "insert query failed"),
array("dbe.selectfail", E_DBE_SELECTFAIL, "select query failed"),
array("dbe.deletefail", E_DBE_DELETEFAIL, "delete query failed")
);
// Get error code by its name
function Errors_ResolveCodeByName (string $name): int {
global $Errors_Enum;
$m = count($Errors_Enum);
for ($i = 0; $i < $m; ++$i) {
if ($Errors_Enum[$i][0] === $name)
return $Errors_Enum[$i][1];
}
return -1;
}
// Get error name by its code
function Errors_ResolveNameByCode (int $id): string {
global $Errors_Enum;
$m = count($Errors_Enum);
for ($i = 0; $i < $m; ++$i) {
if ($Errors_Enum[$i][1] === $id)
return $Errors_Enum[$i][0];
}
return "";
}
// Get error short description by its code
function Errors_ResolveDescByCode (int $id): string {
global $Errors_Enum;
$m = count($Errors_Enum);
for ($i = 0; $i < $m; ++$i) {
if ($Errors_Enum[$i][1] === $id)
return $Errors_Enum[$i][2];
}
return "";
}
?>

27
api/_input_checks.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
// Functions for common input checks
// Check 32 bit integer
function InpChk_IsValidInt32 (&$value): bool {
if (is_null($value))
return false;
if (is_string($value)) {
if (strlen($value) > 24)
return false;
if (!ctype_digit($value))
return false;
$value = intval($value);
}
if ($value > 0xffffffff || $value < -(0xffffffff))
return false;
return true;
}
?>

View File

@@ -1,24 +1,48 @@
<?php
require_once("_errors.php");
// JSON-related functions
function ReturnJSONData ($arr) {
// Includes
if (isset($IS_FRONTEND) && $IS_FRONTEND)
require_once("api/_errorslist.php");
else
require_once("_errorslist.php");
// Write valid JSON data to stdout and exit
function JSON_ReturnData ($arr) {
$data = json_encode($arr);
if (!$data) {
$data = json_encode(array("error" => $Err_Int_JSONEncode));
$data = json_encode(
array(
"error" => Errors_ResolveNameByCode(E_UNS_JSONBADINP)
)
);
}
header("Content-Type: application/json; charset=utf-8");
echo $data;
exit;
}
function ReturnJSONError ($err, $desc) {
ReturnJSONData(array(
"error" => $err,
"description" => $desc
// Return error as JSON data to stdout and exit
function JSON_ReturnError (int $code = -1, string $name = "", string $desc = "") {
if ($code === -1 && empty($name))
JSON_ReturnError(code: E_UNS_INTERNAL, desc: "cant return error without specified code or name");
else if ($code === -1)
$code = Errors_ResolveCodeByName($name);
else if (empty($name))
$name = Errors_ResolveNameByCode($code);
JSON_ReturnData(array(
"error" => $name, // Name
"error_code" => $code, // Code
"error_hum" => Errors_ResolveDescByCode($code), // Common description
"description" => $desc // Detailed decription
));
}
?>

99
api/_types.php Normal file
View File

@@ -0,0 +1,99 @@
<?php
// Necessary functions, types and other stuff
// Includes
if (isset($IS_FRONTEND) && $IS_FRONTEND) {
require_once("api/_errorslist.php");
require_once("api/_json.php");
} else {
require_once("_errorslist.php");
require_once("_json.php");
}
final class ErrorT {
private int $Code;
private string $Name;
private string $Description;
// Ctor
public function __construct(int $code = -1, string $name = "", string $desc = "") {
if ($code === -1 && empty($name))
JSON_ReturnError(code: E_UNS_INTERNAL, desc: "cant construct ErrorT without at least error code or name");
else if ($code === -1)
$code = Errors_ResolveCodeByName($name);
else if (empty($name))
$name = Errors_ResolveNameByCode($code);
$this->Code = $code;
$this->Name = $name;
$this->Description = $desc;
}
// Getter for error code
public function GetCode (): int {
return $this->Code;
}
// Getter for error name
public function GetName (): string {
return $this->Name;
}
// Getter for error description
public function GetDescription (): string {
return $this->Description;
}
// Stringify error
public function Stringify (): string {
if (isset($this->Description))
return "error " . $this->Name . " (" . strval($this->Code) . "): " . $this->Description;
else
return "error " . $this->Name . " (" . strval($this->Code) . ")";
}
}
// Return type of API method
final class ReturnT {
private ErrorT $ErrorObj;
private $Data;
// Ctor
public function __construct($data = null, int $err_code = 0, string $err_name = "", string $err_desc = "") {
$this->ErrorObj = new ErrorT($err_code, $err_name, $err_desc);
$this->Data = $data;
}
// Setter/getter for data
public function SetData ($d) {
$this->Data = $d;
}
public function GetData () {
return $this->Data;
}
// Get string representation of error
public function GetError (): string {
return $this->ErrorObj->Stringify();
}
// Is there any error
public function IsError (): bool {
return $this->ErrorObj->GetCode() !== E_NOERROR;
}
// Throw JSON error
function ThrowJSONError () {
JSON_ReturnError(
$this->ErrorObj->GetCode(),
$this->ErrorObj->GetName(),
$this->ErrorObj->Stringify()
);
}
}
?>

View File

@@ -1,21 +1,47 @@
<?php // Utility functions
<?php
// Utility functions
// Check if request was to specified file
function ThisFileIsRequested ($fullpath): bool {
return substr($fullpath, -strlen($_SERVER["SCRIPT_NAME"])) === $_SERVER["SCRIPT_NAME"];
function Utils_ThisFileIsRequested (string $fullpath): bool {
return (substr($fullpath, -strlen($_SERVER["SCRIPT_NAME"])) === $_SERVER["SCRIPT_NAME"])
|| ($fullpath === $_SERVER["SCRIPT_NAME"]); // Old variant won't work on some configurations, as reported by doesnm
}
// Generate secure random string
function GenerateRandomString (int $length, string $keyspace = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"): string {
if ($length < 1) {
function Utils_GenerateRandomString (int $length, string $keyspace = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"): string {
if ($length < 1)
die("cant generate random string of size less than 1");
}
$pieces = [];
$max = mb_strlen($keyspace, "8bit") - 1;
for ($i = 0; $i < $length; ++$i) {
for ($i = 0; $i < $length; ++$i)
$pieces []= $keyspace[random_int(0, $max)];
}
return implode('', $pieces);
return implode("", $pieces);
}
// Get ratio from two values
function Utils_GetRatio ($x, $y) {
if ($x === $y)
return 1;
return max($x, $y) / min($x, $y);
}
// Join two or more paths pieces to single
function Utils_JoinPaths () {
$paths = array();
foreach (func_get_args() as $arg) {
if ($arg !== "")
$paths[] = $arg;
}
return preg_replace('#/+#', '/', join('/', $paths));
}
// Check if string is valid ASCII
function Utils_IsAscii (string $str): bool {
return (bool)!preg_match("/[\\x80-\\xff]+/", $str);
}
?>

154
api/comments/index.php Normal file
View File

@@ -0,0 +1,154 @@
<?php
// Get all comments from comment section by ID and base methods for managing comment sections
// Includes
if ($IS_FRONTEND) {
require_once("api/_auth.php");
require_once("api/_utils.php");
require_once("api/_errorslist.php");
require_once("api/_types.php");
require_once("api/user/index.php");
} else {
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_errorslist.php");
require_once("../_types.php");
require_once("../user/index.php");
}
// Functions
/*
* FUNCTION
* Get comments amount
*/
function Comments_GetTotalAmount (): int {
global $db;
$qr = $db->query("SELECT COUNT(*) FROM comments");
$row = $qr->fetch_row();
return $row[0];
}
/*
* FUNCTION
* Get comments from range of selected comment section
*/
function Comments_GetSectionRange (int $sec_id, int $ts_from = 0, int $ts_to = 0xffffffff): ReturnT {
global $db, $LOGGED_IN, $THIS_USER;
$result = array();
$s = $db->prepare("SELECT * FROM comments WHERE comment_section_id=? AND created_at>=? AND created_at<=? ORDER BY created_at");
$s->bind_param("iss", $sec_id, date("Y-m-d H:i:s", $ts_from), date("Y-m-d H:i:s", $ts_to));
$s->execute();
$d = $s->get_result();
if (!(bool)$d)
return new ReturnT(data: $result);
// TODO: move this check to method
$isAdmin = false;
if ($LOGGED_IN && User_HasRole($THIS_USER, "admin")->GetData())
$isAdmin = true;
while ($row = $d->fetch_array()) {
if (!$isAdmin && $row["needs_check"])
continue;
$newResultRow = array(
"id" => $row["id"],
"author_id" => $row["author_id"],
"created_at" => $row["created_at"],
"contents" => $row["contents"]
);
if ($isAdmin)
$newResultRow["needs_check"] = (bool)$row["needs_check"];
$result[] = $newResultRow;
}
return new ReturnT(data: $result);
}
// Methods
/*
* METHOD
* Get comments from range of selected comment section
*/
function Comments_GetSectionRange_Method (array $req): ReturnT {
// Input sanity checks
$SectionID = null;
$TSFrom = 0;
$TSTo = 0xffffffff;
if (isset($req["id"])) {
if (!ctype_digit($req["id"]))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "id must be numeric");
$SectionID = intval($req["id"]);
} else {
return new ReturnT(err_code: E_UIN_INSUFARGS, err_desc: "id must be specified");
}
if (isset($req["ts_from"])) {
$TSFrom = $req["ts_from"];
if (strlen($TSFrom) > 24)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "start timestamp cant be this long");
if (!ctype_digit($TSFrom))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "start timestamp must be numeric");
$TSFrom = intval($TSFrom);
if ($TSFrom > 0xffffffff)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "start timestamp cant be bigger than INT32_MAX");
}
if (isset($req["ts_to"])) {
$TSTo = $req["ts_to"];
if (strlen($TSTo) > 24)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "start timestamp cant be this long");
if (!ctype_digit($TSTo))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "start timestamp must be numeric");
$TSTo = intval($TSTo);
if ($TSTo > 0xffffffff)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "start timestamp cant be bigger than INT32_MAX");
}
if ($TSTo < $TSFrom)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "start timestamp cant be bigger than end timestamp");
// Actions
return Comments_GetSectionRange($SectionID, $TSFrom, $TSTo);
}
if (Utils_ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
$result = Comments_GetSectionRange_Method($_REQUEST);
if ($result->IsError())
$result->ThrowJSONError();
else
JSON_ReturnData($result->GetData());
}
?>

349
api/post/create.php Normal file
View File

@@ -0,0 +1,349 @@
<?php
// Create new post
// Includes
if (isset($IS_FRONTEND) && $IS_FRONTEND) {
require_once("api/_auth.php");
require_once("api/_utils.php");
require_once("api/_errorslist.php");
require_once("api/_types.php");
require_once("api/user/index.php");
require_once("api/tags/index.php");
} else {
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_errorslist.php");
require_once("../_types.php");
require_once("../user/index.php");
require_once("../tags/index.php");
}
// Functions
/*
* FUNCTION
* Parse tags from raw string
*/
function Post_ParseRawTagString (string $str): ReturnT {
global $Config;
$allowedSymbols = $Config["posting"]["tags"]["allowed_syms"];
$maxTagLength = $Config["posting"]["tags"]["max_single_length"];
$strLength = strlen($str);
$currLen = 0;
$currTag = "";
$result = array();
for ($i = 0; $i <= $strLength; ++$i) {
if ($i === $strLength || $str[$i] === ",") {
if ($currLen > 0) {
$result[] = $currTag;
$currLen = 0;
$currTag = "";
} else {
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "syntax error at index $i while trying to parse tags");
}
} elseif (!IntlChar::isspace($str[$i])) {
$currTag .= $str[$i];
if (++$currLen > $maxTagLength)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "tag too large: $currTag");
}
}
$preg_str = "/[^" . $allowedSymbols . "]/";
foreach ($result as $tag) {
if (preg_match($preg_str, $tag))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "only allowed symbols in tags are: " . $allowedSymbols);
elseif (!$tag)
unset($tag);
}
return new ReturnT(data: $result);
}
/*
* FUNCTION
* Check if image size properties are valid
*/
function Post_ImgResIsValid (int $x, int $y): bool {
global $Config;
return ($x <= $Config["media"]["max_pic_res"]["x"])
&& ($y <= $Config["media"]["max_pic_res"]["y"])
&& (Utils_GetRatio($x, $y) <= $Config["media"]["max_pic_res"]["ratio"]);
}
/*
* FUNCTION
* Create preview version of image
*/
function Post_CreatePreviewFromImage (string $src, string $dst): ReturnT {
$img = null;
// Reading image from source path
switch (mime_content_type($src)) {
case "image/jpeg":
$img = imagecreatefromjpeg($src);
break;
case "image/png":
$img = imagecreatefrompng($src);
break;
default:
return new ReturnT(err_code: E_UIN_FILETYPE, err_desc: "invalid mime type");
}
// Saving it as LQ JPEG
$saveResult = imagejpeg($img, $dst, 30);
if (!$saveResult) {
if (file_exists($dst)) // $src isnt our responsibility, $dst is
unlink($dst);
return new ReturnT(err_code: E_UNS_UNEXPECTED, err_desc: "failed to create preview");
}
// Check if preview is bigger or same size as original...
if (filesize($src) < filesize($dst))
unlink($dst); // ...then we can just delete preview. Frontend will use preview only when it exist
return new ReturnT(data: true);
}
/*
* FUNCTION
* Stores image and returns paths to picture and its preview
*/
function Post_StoreImage (string $path): ReturnT {
global $Config, $IS_FRONTEND;
// Original extension
$ext = "";
if (mime_content_type($path) === "image/jpeg")
$ext = "jpg";
elseif (mime_content_type($path) === "image/png")
$ext = "png";
else
return new ReturnT(err_code: E_UNS_UNEXPECTED, err_desc: "failed to determine correctly mime type of image");
// Paths
$fileName = strval(time()) . "_" . Utils_GenerateRandomString(4);
// Defining base dir
$baseDir = "";
if (isset($IS_FRONTEND) && $IS_FRONTEND)
$baseDir = "./";
else
$baseDir = "../../";
$targetDir = $baseDir . $Config["media"]["pics_path"];
$targetPath = Utils_JoinPaths($targetDir, $fileName . "." . $ext);
// Moving original picture
$moveResult = move_uploaded_file($path, $targetPath);
if (!$moveResult)
return new ReturnT(err_code: E_UNS_UNEXPECTED, err_desc: "failed to move uploaded file");
// Creating preview file
if ($Config["media"]["previews_enabled"]) {
$previewDir = $baseDir . $Config["media"]["prevs_path"];
$previewPath = Utils_JoinPaths($previewDir, $fileName . ".jpg");
$res = Post_CreatePreviewFromImage($targetPath, $previewPath);
if ($res->IsError()) // $path and $targetPath arent our responsibility, neither is $previewPath
return $res;
if (!file_exists($previewPath)) // If no preview was created - then just nullify path
$previewPath = null;
}
return new ReturnT(data: array(
"preview" => $previewPath,
"picture" => $targetPath
));
}
/*
* FUNCTION
* Create single publication
*/
function Post_Create (
int $author_id,
string $tags,
string $pic_path,
?string $title = null,
?string $prev_path = null,
bool $comms_enabled = false,
bool $edit_lock = false
): ReturnT {
global $db;
$result = null;
// Performing SQL query
$s = $db->prepare("INSERT INTO posts (author_id,tags,title,pic_path,preview_path,comments_enabled,edit_lock) VALUES (?,?,?,?,?,?,?)");
$s->bind_param("issssii", $author_id, $tags, $title, $pic_path, $prev_path, $comms_enabled, $edit_lock);
if ($s->execute() === false)
return new ReturnT(err_code: E_DBE_INSERTFAIL, err_desc: "failed to create post record in DB");
$result = $db->insert_id;
return new ReturnT(data: $result);
}
// Methods
/*
* METHOD
* Create single publication
* Request fields:
* tags - list of tags, should be delimited by comma
* title - optional title for post
* Files fields:
* pic - id of file object in $_FILES variable
* Return value:
* id - unique identifier of created post
*/
function Post_Create_Method (array $req, array $files): ReturnT {
global $Config, $LOGGED_IN, $THIS_USER;
$tags = null;
$pic_path = null;
$title = null;
$prev_path = null;
$comms_enabled = false; // TODO: support for this option at post creation
// Input sanity checks
// Check if user is authenticated
if (!$LOGGED_IN)
return new ReturnT(err_code: E_AUT_NOTAUTHED, err_desc: "you must be logged in to create posts");
// Check if there are necessary input
if (!isset($req["tags"]) || !isset($files["pic"]) || is_array($files["pic"]["error"]) || $files["pic"]["error"] === UPLOAD_ERR_NO_FILE)
return new ReturnT(err_code: E_UIN_INSUFARGS, err_desc: "tags and picture are necessary");
// Check tags
// If raw string length not fits into limit
$tagsLen = strlen($req["tags"]);
$tagsMaxLen = $Config["posting"]["tags"]["max_raw_input_str_length"];
if ($tagsLen > $tagsMaxLen)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "tags string length exceeds limit: " . $tagsMaxLen);
elseif ($tagsLen < 1)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "tags cant be empty");
// Check if supplied string is ASCII
if (!Utils_IsAscii($req["tags"]))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "tags must be ASCII-only");
// Parsing tags
$parsedTags = Post_ParseRawTagString($req["tags"]);
if ($parsedTags->IsError())
return $parsedTags;
$parsedTags = $parsedTags->GetData();
// Check if tags are approved
foreach ($parsedTags as $singleTag) {
if (!Tags_IsTagApproved($singleTag))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "tag \"$singleTag\" is not approved");
}
// Concatenating parsed tags to single comma-separated string
$tags = implode(",", $parsedTags);
// Check user role TODO: add rate-limiting, instead of this
if (User_HasRole($THIS_USER, "newbie")->GetData())
return new ReturnT(err_code: E_ACS_INSUFROLE, err_desc: "newbies cant create posts");
// Checking title
if (isset($req["title"])) {
// Title length
$maxTitleLen = $Config["posting"]["title"]["max_length"];
$realTitleLen = strlen($req["title"]);
if ($realTitleLen > $maxTitleLen)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "title length exceeds maximum value");
// Cleaning off all bad symbols (no script injection allowed here) TODO: move to function
for ($i = 0; $i < $realTitleLen; ++$i) {
switch ($req["title"][$i]) {
case "<":
$title .= "&lt;";
break;
case ">":
$title .= "&gt;";
break;
case "/":
$title .= "&#47;";
break;
case "\\":
$title .= "&#92;";
break;
case "?":
$title .= "&#63;";
break;
case "&":
$title .= "&amp;";
break;
case "\n":
$title .= "<br>";
break;
case "\t":
$title .= "&emsp;";
break;
default:
$title .= $req["title"][$i];
}
}
if (strlen($title) > $maxTitleLen)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "title length exceeds maximum value after escaping");
}
// Check image properties
// If error happened while uploading
if ($files["pic"]["error"] !== UPLOAD_ERR_OK)
return new ReturnT(err_code: E_UIN_FAIL2UPLD, err_desc: "error while uploading file: " . strval($files["pic"]["error"]));
// If size is too large
if ($files["pic"]["size"] > $Config["media"]["max_pic_size"])
return new ReturnT(err_code: E_UIN_FILE2LARGE, err_desc: "picture size is too large");
$TmpFilePath = $files["pic"]["tmp_name"];
// If file mime type is not in list of allowed
if (!in_array(mime_content_type($TmpFilePath), $Config["media"]["allowed_mimetypes"]))
return new ReturnT(err_code: E_UIN_FILETYPE, err_desc: "picture mime type is invalid");
// Check if resolution is bigger than allowed or have unacceptable aspect ratio
list($SzX, $SzY, $Type, $Attr) = getimagesize($TmpFilePath);
if (!Post_ImgResIsValid($SzX, $SzY))
return new ReturnT(err_code: E_UIN_IMGBADRES, err_desc: "image with that resolution or aspect ratio cant be accepted");
// Actions
// Copy picture to storage folder
$res = Post_StoreImage($TmpFilePath);
if ($res->IsError()) { // $TmpFilePath seemingly isnt our responsibility, BUT, only we know how and what to cleanup
unlink($TmpFilePath);
return $res;
}
$res = $res->GetData();
$pic_path = $res["picture"];
$prev_path = $res["preview"];
$res = Post_Create($THIS_USER, $tags, $pic_path, $title, $prev_path, $comms_enabled, false);
if ($res->IsError()) { // Cleaning up all processed pics
unlink($pic_path);
if ($prev_path)
unlink($prev_path);
}
return $res;
}
if (Utils_ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
$result = Post_Create_Method($_POST, $_FILES);
// Checking result
if ($result->IsError())
$result->ThrowJSONError();
else
JSON_ReturnData(["id" => $result->GetData()]);
}
?>

307
api/post/find.php Normal file
View File

@@ -0,0 +1,307 @@
<?php
// Search posts
// Includes
if (isset($IS_FRONTEND) && $IS_FRONTEND) {
require_once("api/_config.php");
require_once("api/_auth.php");
require_once("api/_utils.php");
require_once("api/_input_checks.php");
require_once("api/_errorslist.php");
require_once("api/_types.php");
require_once("api/user/index.php");
require_once("api/post/index.php");
} else {
require_once("../_config.php");
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_input_checks.php");
require_once("../_errorslist.php");
require_once("../_types.php");
require_once("../user/index.php");
require_once("./index.php");
}
// Functions
/*
* FUNCTION
* Get list of posts from range. Almost no checks of arguments are performed, so beware
* Arguments:
* offset - offset from start
* amount - amount of posts to return
*/
function Post_GetPostsFromRange (?int $offset = null, ?int $amount = null): ReturnT {
global $db, $Config;
$result = array();
// Managing defaults
if (is_null($offset))
$offset = 0;
if (empty($amount))
$amount = $Config["max_posts_per_request"];
// Get posts from db in range
$statement = $db->prepare("SELECT * FROM posts LIMIT ?, ?");
$statement->bind_param("ii", $offset, $amount);
$statement->execute();
if (($queryResult = $statement->get_result()) === false)
return new ReturnT(err_code: E_DBE_UNKNOWN);
$result = $queryResult->fetch_all(MYSQLI_ASSOC);
return new ReturnT(data: $result);
}
/*
* FUNCTION
* Get list of posts matching criteria. No additional checks of arguments are performed
* Arguments:
* tags - must be array of valid tags or null
* author_ids - must be array of valid author ids or null
* ts_after - valid starting timestamp for filtering by time, that less or equal to ts_before, or null
* ts_before - valid ending timestamp for filtering by time, that bigger or equal to ts_after, or null
*/
function Post_GetMatchingPosts (
?array $tags = null,
?array $author_ids = null,
?int $ts_after = null,
?int $ts_before = null
): ReturnT {
global $db;
$result = array();
// Managing defaults
if (is_null($ts_after))
$ts_after = 0;
if (is_null($ts_before))
$ts_before = 0xffffffff;
$dateFrom = date("Y-m-d H:i:s", $ts_after);
$dateTo = date("Y-m-d H:i:s", $ts_before);
// Get posts from db in time range
$s = $db->prepare("SELECT * FROM posts WHERE created_at>=? AND created_at<=?");
$s->bind_param("ss", $dateFrom, $dateTo);
$s->execute();
$d = $s->get_result();
// Filter them out
// NOTICE: ~~skill~~ perf issue, will wildly affect response time and memory usage on big sets
// Filter by author, if needed
$needToFilterByAuthor = !empty($author_ids);
$tempFilteredByAuthor = array();
// If post author is any author from list - we take it
while ($row = $d->fetch_array()) {
if (!$needToFilterByAuthor || ($needToFilterByAuthor && in_array($row["author_id"], $author_ids)))
$tempFilteredByAuthor[] = array( // NOTICE: this should look better
"id" => $row["id"],
"author_id" => $row["author_id"],
"comment_section_id" => $row["comment_section_id"],
"created_at" => $row["created_at"],
"tags" => $row["tags"],
"title" => $row["title"],
"votes_up" => $row["votes_up"],
"votes_down" => $row["votes_down"],
"views" => $row["views"],
"pic_path" => $row["pic_path"],
"preview_path" => $row["preview_path"],
"comments_enabled" => $row["comments_enabled"],
"edit_lock" => $row["edit_lock"]
);
}
if (!count($tempFilteredByAuthor))
return new ReturnT(data: $result);
// Filter by tags
// If post has all of the tags from list - we take it
foreach ($tempFilteredByAuthor as $post) {
$fitsFilter = true;
foreach ($tags as $singleTag) {
if (!str_contains($post["tags"], $singleTag)) {
$fitsFilter = false;
break;
}
}
if ($fitsFilter)
$result[] = $post;
}
// Return result
return new ReturnT(data: $result);
}
/*
* FUNCTION
* Parse raw query to list of tags and author IDs. Checks on encoding are not performed
* Arguments:
* query - ASCII query string
*/
function Post_ParseRawQuery (string $query): ReturnT {
global $Config;
$result = array(
"tags" => array(),
"author_ids" => array()
);
$allowedTagSymbols = $Config["posting"]["tags"]["allowed_syms"];
$badTagSymbolsPreg = "/[^" . $allowedTagSymbols . "]/";
$allowedLoginSymbols = $Config["registration"]["allowed_syms"];
$badLoginSymbolsPreg = "/[^" . $allowedLoginSymbols . "]/";
$maxTagLength = $Config["posting"]["tags"]["max_single_length"];
$queryLength = strlen($query);
$currWord = "";
$currWordLen = 0;
$isAuthor = false;
// Parse everything
for ($i = 0; $i <= $queryLength; ++$i) {
if ($i === $queryLength || $query[$i] === ",") { // If end of query or comma
// NOTICE: potential fix ` || (IntlChar::isspace($query[$i]) && $isAuthor)`
// NOTICE: currently, query tags are separated by comma, but may be i should make it by space
if ($currWordLen > 0) { // If we have some word
if ($isAuthor) { // If word is author meta-field
$isAuthor = false;
if (preg_match($badLoginSymbolsPreg, $currWord)) // Unallowed symbols in login are detected
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "only allowed symbols in logins are \"$allowedLoginSymbols\"");
$userIDRet = User_GetIDByLogin($currWord); // Fetching user ID by login
if ($userIDRet->IsError())
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "user $currWord does not exist");
else
$result["author_ids"][] = $userIDRet->GetData();
} else { // If word is tag
$result["tags"][] = $currWord;
}
// Reset current word
$currWordLen = 0;
$currWord = "";
} else { // If malformed query
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "syntax error at index $i: starting/ending comma, sequence of commas or missing meta-field value");
}
} elseif ($query[$i] === ":") { // Semicolon means this is meta-field
if (strtolower($currWord) === "author") { // If meta-field is author
$isAuthor = true; // Set author meta-field flag
// Reset word
$currWordLen = 0;
$currWord = "";
} else { // Invalid metafield
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "syntax error at index $i: invalid meta-field name \"$currWord\"");
}
} elseif (!preg_match($badTagSymbolsPreg, $query[$i]) || $isAuthor) { // If any valid non-special symbol OR we parsing login now
$currWord .= $query[$i];
if (++$currWordLen > $maxTagLength)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "word too large: $currWord");
} elseif (!IntlChar::isspace($query[$i])) { // If we have something that is not whitespace
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "unexpected symbol at index $i: " . $query[$i]);
}
}
return new ReturnT(data: $result);
}
// Methods
/*
* METHOD
* Returns list of posts from supplied range based on supplied raw filter parameters
* Request fields:
* query - raw query string
* offset - beginning of posts range
* amount - number of posts to get (optional)
*/
function Post_GetMatchingPosts_Method (array $req): ReturnT {
global $Config;
$cfgMaxPostsPerRequest = $Config["max_posts_per_request"];
$reqQuery = null;
$reqOffset = null;
$reqAmount = null;
// TODO: filter by time range
// Input sanity checks
// Generic checks
if (isset($req["offset"])) {
$reqOffset = $req["offset"];
if (!InpChk_IsValidInt32($reqOffset))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "invalid offset value: $reqOffset");
} else {
$reqOffset = 0;
}
if (isset($req["amount"])) {
$reqAmount = $req["amount"];
if (!InpChk_IsValidInt32($reqAmount))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "invalid amount value: $reqAmount");
} else {
$reqAmount = $cfgMaxPostsPerRequest; // TODO: account defaults
}
// Specific checks
if ($reqOffset < 0)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "offset must be zero or bigger");
if ($reqAmount < 1)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "posts amount must be bigger than 1");
if ($reqAmount > $cfgMaxPostsPerRequest)
$reqAmount = $cfgMaxPostsPerRequest;
// Generic check again
if (!isset($req["query"])) {
$result = Post_GetPostsFromRange($reqOffset, $reqAmount);
if ($result->IsError())
return $result;
$resData = $result->GetData();
return new ReturnT(data: array( // Just return posts from range, without filtering
"data" => $resData,
"total_amount" => count($resData)
));
}
$reqQuery = $req["query"];
// Check query and parse it to array
if (!Utils_IsAscii($reqQuery))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "query must be ASCII string");
$qr = Post_ParseRawQuery($reqQuery);
if ($qr->IsError())
return $qr;
$query = $qr->GetData();
// Actions
// NOTICE: perf issue
$result = Post_GetMatchingPosts($query["tags"], $query["author_ids"]);
if ($result->IsError())
return $result;
$resData = $result->GetData();
return new ReturnT(data: array(
"data" => array_slice($resData, $reqOffset, $reqAmount),
"total_amount" => count($resData) // NOTICE: very shitty design
));
}
if (Utils_ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
$result = Post_GetMatchingPosts_Method($_REQUEST);
if ($result->IsError())
$result->ThrowJSONError();
else
JSON_ReturnData($result->GetData());
}
?>

View File

@@ -1,24 +1,70 @@
<?php // Get single post by ID
require_once("../_auth.php");
require_once("../_utils.php");
<?php
// Get single post by ID
// Get single publication by ID
function Post_GetByID ($id) {
// Includes
if (isset($IS_FRONTEND) && $IS_FRONTEND) {
require_once("api/_auth.php");
require_once("api/_utils.php");
require_once("api/_errorslist.php");
require_once("api/_types.php");
} else {
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_errorslist.php");
require_once("../_types.php");
}
// Functions
/*
* FUNCTION
* Get total amount of posts
*/
function Post_GetPostsAmount (): int {
global $db;
$qr = $db->query("SELECT COUNT(*) FROM posts");
$row = $qr->fetch_row();
return $row[0];
}
/*
* FUNCTION
* Increment number of views for post
*/
function Post_AddView (int $id): ReturnT {
global $db;
$s = $db->prepare("UPDATE posts SET views = views + 1 WHERE id = ?");
$s->bind_param("i", $id);
if (!$s->execute())
return new ReturnT(err_code: E_DBE_UNKNOWN, err_desc: "failed to execute statement");
return new ReturnT(data: true);
}
/*
* FUNCTION
* Get post information by ID
*/
function Post_GetByID (int $id): ReturnT {
global $db;
$result = array();
$s = $db->prepare("SELECT * FROM posts WHERE id = ?");
$s->bind_param("s", $id);
$s->bind_param("i", $id);
$s->execute();
$d = $s->get_result()->fetch_assoc();
if (!(bool)$d) {
return null;
}
if (!(bool)$d)
return new ReturnT(err_code: E_UIN_WRONGID, err_desc: "failed to get post");
$result["id"] = $d["id"];
$result["author_id"] = $d["author_id"];
@@ -34,31 +80,49 @@ function Post_GetByID ($id) {
$result["preview_path"] = $d["preview_path"];
$result["edit_lock"] = $d["edit_lock"];
// TODO: increment views of post
$r = Post_AddView($id); // TODO: add rate-limit or completely rework
if ($r->IsError())
return $r;
return $result;
return new ReturnT(data: $result);
}
if (ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
// Methods
if (isset($_REQUEST["id"])) {
if (!ctype_digit($_REQUEST["id"]))
ReturnJSONError($Err_RDP_InvalidID, "id must be numeric");
$UserID = intval($_REQUEST["id"]);
/*
* METHOD
* Get post information by ID
*/
function Post_GetByID_Method (array $req): ReturnT {
// Input sanity checks
$PostID = null;
if (isset($req["id"])) {
if (!ctype_digit($req["id"]))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "id must be numeric");
$PostID = intval($req["id"]);
} else {
ReturnJSONError($Err_RDP_InvalidID, "id must be specified");
return new ReturnT(err_code: E_UIN_INSUFARGS, err_desc: "id must be specified");
}
// TODO: check permissions
// Actions
$ResponseData = Post_GetByID($_REQUEST["id"]);
if ($ResponseData)
ReturnJSONData($ResponseData);
return Post_GetByID($PostID);
}
if (Utils_ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
$result = Post_GetByID_Method($_REQUEST);
if ($result->IsError())
$result->ThrowJSONError();
else
ReturnJSONError($Err_DP_IDNotFound, "wrong id");
JSON_ReturnData($result->GetData());
}
?>

139
api/tags/index.php Normal file
View File

@@ -0,0 +1,139 @@
<?php
// Get info about tags
// Includes
if (isset($IS_FRONTEND) && $IS_FRONTEND) {
require_once("api/_auth.php");
require_once("api/_utils.php");
require_once("api/_errorslist.php");
require_once("api/_types.php");
} else {
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_errorslist.php");
require_once("../_types.php");
}
// Functions
/*
* FUNCTION
* Get amount of approved tags
*/
function Tags_GetTagsAmount (): int {
global $db;
$qr = $db->query("SELECT COUNT(*) FROM approved_tags");
$row = $qr->fetch_row();
return $row[0];
}
/*
* FUNCTION
* Get list of all approved tags
*/
function Tags_GetListOfApproved (): array {
global $db;
$result = array();
$s = $db->prepare("SELECT * FROM approved_tags ORDER BY value");
$s->execute();
$d = $s->get_result();
if (!(bool)$d)
return $result;
while ($row = $d->fetch_array()) {
$result[] = array(
"value" => $row["value"],
"author_id" => $row["author_id"],
"added_at" => $row["added_at"]
);
}
return $result;
}
/*
* FUNCTION
* Check if tag is in list of approved tags
*/
function Tags_IsTagApproved (string $str): bool {
global $db;
$s = $db->prepare("SELECT value FROM approved_tags WHERE value = ?");
$s->bind_param("s", $str);
$s->execute();
return (bool)$s->get_result()->fetch_assoc();
}
// Methods
/*
* METHOD
* Check if tag is approved
*/
function Tags_IsTagApproved_Method (array $req): ReturnT {
global $Config;
$requestedTag = $req["value"];
// Input sanity checks
// Check length
$maxTagLen = $Config["posting"]["tags"]["max_single_length"];
$realLen = strlen($requestedTag);
if ($realLen > $maxTagLen)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "tag length must be equal to or less than " . strval($maxTagLen));
elseif ($realLen < 1)
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "tag can not be empty");
// Check symbols
$allowedSymbols = $Config["posting"]["tags"]["allowed_syms"];
$preg_str = "/[^" . $allowedSymbols . "]/";
if (preg_match($preg_str, $requestedTag))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "only allowed symbols in tags are: " . $allowedSymbols);
// Actions
return new ReturnT(data: array(
"tag" => $requestedTag,
"is_approved" => Tags_IsTagApproved($requestedTag)
));
}
/*
* METHOD
* Get list of all approved tags
*/
function Tags_GetListOfApproved_Method (): ReturnT {
// Actions
return new ReturnT(data: Tags_GetListOfApproved());
}
if (Utils_ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
if (isset($_REQUEST["value"]))
$result = Tags_IsTagApproved_Method($_REQUEST);
else
$result = Tags_GetListOfApproved_Method();
if ($result->IsError())
$result->ThrowJSONError();
else
JSON_ReturnData($result->GetData());
}
?>

View File

@@ -0,0 +1,29 @@
<?php
// Start session as any user
// ATTENTION: FOR DEBUG PURPOSES ONLY!
if ($IS_FRONTEND)
die("this file must not be included!");
// Includes
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_errorslist.php");
if (Utils_ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
if (!$Config["debug"])
JSON_ReturnError(code: E_UNS_INTERNAL, desc: "you need to enable debug mode in configuration file first");
if (!isset($_REQUEST["id"]))
JSON_ReturnError(code: E_UIN_INSUFARGS, desc: "valid id must be specified");
$_SESSION["userid"] = intval($_REQUEST["id"]);
JSON_ReturnData($_SESSION);
}
?>

View File

@@ -1,82 +1,139 @@
<?php // Creating account
require_once("../_auth.php");
require_once("../_utils.php");
require_once("./index.php");
<?php
// Creating account
// Create new user account
function User_Create ($login, $password, $email = null, $invite_id = null, $avatar_path = null): bool {
// Includes
if ($IS_FRONTEND) {
require_once("api/_auth.php");
require_once("api/_utils.php");
require_once("api/_errorslist.php");
require_once("api/_types.php");
require_once("api/user/index.php");
} else {
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_errorslist.php");
require_once("../_types.php");
require_once("./index.php");
}
// Functions
/*
* FUNCTION
* Create new user account
*/
function User_Create (string $login, string $password, ?string $email = null, ?string $invite_id = null, ?string $avatar_path = null): ReturnT {
global $db;
$salt = GenerateRandomString(8);
$salt = Utils_GenerateRandomString(8);
$pwd_hash = hash("sha256", $password . $salt, true);
// TODO: process invite
$s = $db->prepare("INSERT INTO users (login,email,password_hash,salt,avatar_path,role,invite_id) VALUES (?,?,?,?,?,?,?)");
$role = "newbie";
$role = "newbie"; // TODO: make decision from config or supply by argument
$s->bind_param("sssssss", $login, $email, $pwd_hash, $salt, $avatar_path, $role, $invite_id);
return $s->execute() !== false;
if ($s->execute() === false)
return new ReturnT(err_code: E_DBE_INSERTFAIL, err_desc: "cant insert record to users DB");
return new ReturnT(data: true);
}
if (ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
// Methods
// If registration turned off
if (!$Config["registration"]["active"]) {
ReturnJSONError($Err_DP_RegClosed, "registrations are closed");
}
/*
* METHOD
* Create new user account
*/
function User_Create_Method (array $req): ReturnT {
global $Config, $LOGGED_IN;
$login = null;
$password = null;
$email = null;
$invite_id = null;
$avatar_path = null;
// Input sanity checks
// If registration is turned off
if (!$Config["registration"]["active"])
return new ReturnT(err_code: E_AUT_REGCLOSED);
// If user is logged in, then we should not allow creation of account
if ($LOGGED_IN)
ReturnJSONError($Err_DP_AlreadyLoggedIn, "you are already logged in");
return new ReturnT(err_code: E_AUT_ALRLOGIN);
// If we have some POST data
if (isset($_POST["login"]) && isset($_POST["password"])) {
$login = $_POST["login"];
$password = $_POST["password"];
$email = null;
$invite = null;
// If we have some base data
if (isset($req["login"]) && isset($req["password"])) {
$login = $req["login"];
$password = $req["password"];
// If password is too weak
if (strlen($password) < 8)
ReturnJSONError($Err_RDP_InvalidArgs, "password too weak");
if (strlen($password) < $Config["registration"]["min_passw_len"])
return new ReturnT(err_code: E_AUT_PWD2WEAK, err_desc: "password must contain at least " . strval($Config["registration"]["min_passw_len"]) . " characters");
// If we need email but it isnt supplied
if ($Config["registration"]["need_email"] && !isset($_POST["email"])) {
ReturnJSONError($Err_RDP_InvalidArgs, "email is necessary");
} elseif (isset($_POST["email"])) {
if ($Config["registration"]["need_email"] && !isset($req["email"])) {
return new ReturnT(err_code: E_UIN_INSUFARGS, err_desc: "email is necessary");
} elseif (isset($req["email"])) {
// Validation of email
if (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL))
ReturnJSONError($Err_RDP_InvalidArgs, "email is invalid");
$email = $_POST["email"];
if (!filter_var($req["email"], FILTER_VALIDATE_EMAIL))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "email is invalid");
$email = $req["email"];
}
// If we need invite but it isnt supplied
if ($Config["registration"]["need_invite"] && !isset($_POST["invite_id"])) {
ReturnJSONError($Err_RDP_InvalidArgs, "registrations are invite-only");
} elseif (isset($_POST["invite_id"])) {
if ($Config["registration"]["need_invite"] && !isset($req["invite_id"])) {
return new ReturnT(err_code: E_UIN_INSUFARGS, err_desc: "registrations are invite-only, you need to specify invite ID");
} elseif (isset($req["invite_id"])) {
// TODO: check invite and reject if it invalid
//$invite = $_POST["invite_id"];
//$invite_id = $req["invite_id"];
return new ReturnT(err_code: E_UNS_NOTIMPL, err_desc: "invitations are not implemented yet");
}
// Check login and password for pattern match
$preg_str = "/[^" . $Config["registration"]["allowed_syms"] . "]/";
if (preg_match($preg_str, $login) || preg_match($preg_str, $password)) {
ReturnJSONError($Err_RDP_InvalidArgs, "only allowed symbols are: " . $Config["registration"]["allowed_syms"]);
}
if (preg_match($preg_str, $login) || preg_match($preg_str, $password))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "only allowed symbols in login and password are: " . $Config["registration"]["allowed_syms"]);
// Check if login already exists
if (User_LoginExist($login))
ReturnJSONError($Err_RDP_InvalidArgs, "login already exists");
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "login already exists");
$result = User_Create($login, $password, $email, $invite);
ReturnJSONData(["success" => $result]);
// TODO: check $avatar_path
} else { // Not enough arguments
ReturnJSONError($Err_RDP_InvalidArgs, "not enough or no arguments were supplied");
return new ReturnT(err_code: E_UIN_INSUFARGS, err_desc: "not enough or no arguments were supplied");
}
// Actions
return User_Create($login, $password, $email, $invite_id, $avatar_path);
}
if (Utils_ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
// HACK: for debugging purposes. Will be removed later
if ($Config["debug"])
$_POST = $_REQUEST;
// Create account
$result = User_Create_Method($_POST);
// Checking result
if ($result->IsError())
$result->ThrowJSONError();
else
JSON_ReturnData(["success" => $result->GetData()]);
}
?>

View File

@@ -1,34 +1,90 @@
<?php
require_once("../_auth.php");
require_once("../_utils.php");
require_once("./index.php");
// Deleting existing account
function User_Delete($id){
global $db;
$s = $db->prepare("delete from users where id = $id");
$s->bind_param("s",$id);
return $s->execute() !== false;
// Includes
if ($IS_FRONTEND) {
require_once("api/_auth.php");
require_once("api/_utils.php");
require_once("api/_errorslist.php");
require_once("api/user/index.php");
} else {
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_errorslist.php");
require_once("./index.php");
}
if (ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
if (isset($_REQUEST["id"])) {
if (!ctype_digit($_REQUEST["id"]))
ReturnJSONError($Err_RDP_InvalidID, "id must be numeric");
if(!User_HasRole("admin")){
ReturnJSONError($Err_DP_NotEnoughRole,"You need to be admin to delete other accounts");
}
$UserID = intval($_REQUEST["id"]);
// Functions
/*
* FUNCTION
* Delete existing account
*/
function User_Delete (int $id): ReturnT {
global $db;
$s = $db->prepare("delete from users where id = ?");
$s->bind_param("s", $id);
return new ReturnT(data: ($s->execute() !== false));
}
// Methods
/*
* METHOD
* Delete existing account
*/
function User_Delete_Method (array $req): ReturnT {
global $LOGGED_IN, $THIS_USER;
$id = null;
// Input sanity checks
if (isset($req["id"]) && $LOGGED_IN) {
if (!ctype_digit($req["id"]))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "id must be numeric");
$id = intval($req["id"]);
} elseif (!isset($req["id"]) && $LOGGED_IN) {
$id = $THIS_USER;
} else {
if ($LOGGED_IN)
$UserID = $_SESSION["userid"];
else
ReturnJSONError($Err_RDP_InvalidID, "id must be specified or valid session must be provided");
return new ReturnT(err_code: E_AUT_NOTAUTHED, err_desc: "valid session must be provided");
}
$result = User_Delete($UserID);
session_unset();
session_destroy();
ReturnJSONData(["success" => $result]);
// If its attempt to delete other account
if (!User_HasRole($THIS_USER, "admin")->GetData() && $THIS_USER !== $id)
return new ReturnT(err_code: E_ACS_INSUFROLE, err_desc: "you must be admin to delete other accounts");
// Actions
return User_Delete($id);
}
if (Utils_ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
// HACK: for debugging purposes. Will be removed later
if ($Config["debug"])
$_POST = $_REQUEST;
$result = User_Delete_Method($_POST);
if ($result->IsError()) {
$result->ThrowJSONError();
} else {
// If it was self-deletion
if ($id === $THIS_USER)
AUTH_EndSession();
JSON_ReturnData(["success" => $result->GetData()]);
}
}
?>

View File

@@ -1,70 +1,146 @@
<?php // Viewing account data
require_once("../_auth.php");
require_once("../_utils.php");
<?php
// Viewing account data
// Check if user with supplied login exists
function User_LoginExist ($login): bool {
// Includes
if (isset($IS_FRONTEND) && $IS_FRONTEND) {
require_once("api/_auth.php");
require_once("api/_utils.php");
require_once("api/_errorslist.php");
require_once("api/_types.php");
} else {
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_errorslist.php");
require_once("../_types.php");
}
// Functions
/*
* FUNCTION
* Get amount of users
*/
function User_GetUsersAmount (): array {
global $db;
$s = $db->prepare("SELECT * FROM users WHERE login = ?");
$result = array(
"users" => 0,
"banned" => 0
);
$qr = $db->query("SELECT COUNT(*) FROM users");
$row = $qr->fetch_row();
$result["users"] = $row[0];
$qr = $db->query("SELECT COUNT(*) FROM users WHERE banned = TRUE");
$row = $qr->fetch_row();
$result["banned"] = $row[0];
return $result;
}
/*
* FUNCTION
* Check if user with supplied login exists
*/
function User_LoginExist (string $login): bool {
global $db;
$s = $db->prepare("SELECT id FROM users WHERE login = ?");
$s->bind_param("s", $login);
$s->execute();
return (bool)$s->get_result()->fetch_assoc();
}
// Check if user has specified role
function User_HasRole ($id, $role): bool {
/*
* FUNCTION
* Check if user with supplied ID exists
*/
function User_IDExist (int $id): bool {
global $db;
$s = $db->prepare("SELECT * FROM users WHERE id = ?");
$s = $db->prepare("SELECT id FROM users WHERE id = ?");
$s->bind_param("s", $id);
$s->execute();
return (bool)$s->get_result()->fetch_assoc();
}
/*
* FUNCTION
* Check if user has specified role
*/
function User_HasRole (int $id, string $role): ReturnT {
global $db;
$s = $db->prepare("SELECT role FROM users WHERE id = ?");
$s->bind_param("s", $id);
$s->execute();
$d = $s->get_result()->fetch_assoc();
if (!(bool)$d) {
return null;
}
if (!(bool)$d)
return new ReturnT(err_code: E_UIN_WRONGID, err_desc: "user not found in database");
if ($d["role"] == $role) {
return true;
}
return false;
return new ReturnT(data: $d["role"] === $role);
}
// Check if user is moderator
function User_IsMod ($id) {
/*
* FUNCTION
* Check if user is moderator (or higher)
*/
function User_IsMod (int $id): ReturnT {
global $db;
$s = $db->prepare("SELECT * FROM users WHERE id = ?");
$s = $db->prepare("SELECT role FROM users WHERE id = ?");
$s->bind_param("s", $id);
$s->execute();
$d = $s->get_result()->fetch_assoc();
if (!(bool)$d) {
return null;
}
if (!(bool)$d)
return new ReturnT(err_code: E_UIN_WRONGID, err_desc: "user not found in database");
return in_array($d["role"], array("mod", "admin"));
return new ReturnT(data: in_array($d["role"], array("mod", "admin")));
}
// Get user information from DB
function User_GetInfoByID ($id) {
/*
* FUNCTION
* Get user ID by login
*/
function User_GetIDByLogin (string $login): ReturnT {
global $db;
$s = $db->prepare("SELECT * FROM users WHERE login = ?");
$s->bind_param("s", $login);
$s->execute();
$d = $s->get_result()->fetch_assoc();
if (!(bool)$d)
return new ReturnT(err_code: E_UIN_WRONGID, err_desc: "user not found in database");
return new ReturnT(data: $d["id"]);
}
/*
* FUNCTION
* Get user information from DB by supplied ID
*/
function User_GetInfoByID (int $id): ReturnT {
global $db, $THIS_USER, $LOGGED_IN;
$result = array();
$s = $db->prepare("SELECT * FROM users WHERE id = ?");
$s->bind_param("s", $id);
$s->bind_param("i", $id);
$s->execute();
$d = $s->get_result()->fetch_assoc();
if (!(bool)$d) {
return null;
}
if (!(bool)$d)
return new ReturnT(err_code: E_UIN_WRONGID, err_desc: "user not found in database");
$result["id"] = $d["id"];
$result["created_at"] = $d["created_at"];
@@ -72,37 +148,65 @@ function User_GetInfoByID ($id) {
$result["avatar_path"] = $d["avatar_path"];
$result["role"] = $d["role"];
$result["banned"] = $d["banned"];
if ($id === $_SESSION["userid"] || User_IsMod($_SESSION["userid"])) { // User himself and mods can see additional info
// User himself and mods can see additional info
if ($id === $THIS_USER) {
$result["email"] = $d["email"];
$result["invite_id"] = $d["invite_id"];
} elseif ($LOGGED_IN) {
if (User_IsMod($THIS_USER)->GetData()) {
$result["email"] = $d["email"];
$result["invite_id"] = $d["invite_id"];
}
}
return $result;
return new ReturnT(data: $result);
}
if (ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
// Methods
/*
* METHOD
* Get user information from DB
* Request fields:
* id - user id
*/
function User_GetInfoByID_Method (array $req): ReturnT {
global $THIS_USER, $LOGGED_IN;
// Input sanity checks
$UserID = null;
if (isset($_REQUEST["id"])) {
if (!ctype_digit($_REQUEST["id"]))
ReturnJSONError($Err_RDP_InvalidID, "id must be numeric");
$UserID = intval($_REQUEST["id"]);
if (isset($req["id"])) {
if (!ctype_digit($req["id"]))
return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "id must be numeric");
$UserID = intval($req["id"]);
} else {
if ($LOGGED_IN)
$UserID = $_SESSION["userid"];
$UserID = $THIS_USER;
else
ReturnJSONError($Err_RDP_InvalidID, "id must be specified or valid session must be provided");
return new ReturnT(err_code: E_UIN_INSUFARGS, err_desc: "id must be specified or valid session must be provided");
}
$ResponseData = User_GetInfoByID($UserID);
if ($ResponseData)
ReturnJSONData($ResponseData);
else
ReturnJSONError($Err_DP_IDNotFound, "wrong id");
// Actions
return User_GetInfoByID($UserID);
}
if (Utils_ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
$result = User_GetInfoByID_Method($_REQUEST);
if ($result->IsError())
$result->ThrowJSONError();
else
JSON_ReturnData($result->GetData());
}
?>

89
api/user/login.php Normal file
View File

@@ -0,0 +1,89 @@
<?php
// Logging into account
// Includes
if ($IS_FRONTEND) {
require_once("api/_auth.php");
require_once("api/_utils.php");
require_once("api/_errorslist.php");
require_once("api/_types.php");
require_once("api/user/index.php");
} else {
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_errorslist.php");
require_once("../_types.php");
require_once("./index.php");
}
// Methods
/*
* METHOD
* Log into existing user account
*/
function User_Login_Method (array $req): ReturnT {
global $db, $LOGGED_IN, $THIS_USER;
$login = $req["login"];
$password = $req["password"];
// Input sanity checks
// If already logged in
if ($LOGGED_IN)
return new ReturnT(err_code: E_AUT_ALRLOGIN, err_desc: "you are already logged in");
// If no password or login supplied
if (!isset($login) || !isset($password))
return new ReturnT(err_code: E_AUT_WRONGCREDS, err_desc: "you must supply both login and password");
// Checking if password is correct
$s = $db->prepare("SELECT id,password_hash,salt FROM users WHERE login = ?");
$s->bind_param("s", $login);
$s->execute();
$d = $s->get_result()->fetch_assoc();
// Wrong login
if (!(bool)$d)
return new ReturnT(err_code: E_AUT_WRONGCREDS, err_desc: "wrong login or password");
$suppl_pwd_hash = hash("sha256", $password . $d["salt"], true);
$real_pwd_hash = $d["password_hash"];
// Wrong password
if ($suppl_pwd_hash !== $real_pwd_hash)
return new ReturnT(err_code: E_AUT_WRONGCREDS, err_desc: "wrong login or password");
// Actions
$_SESSION["userid"] = $d["id"];
$THIS_USER = $d["id"];
return new ReturnT(data: true);
}
if (Utils_ThisFileIsRequested(__FILE__)) {
require_once("../_json.php");
// HACK: for debugging purposes. Will be removed later
if ($Config["debug"])
$_POST = $_REQUEST;
// Log into account
$result = User_Login_Method($_POST);
// Checking result
if ($result->IsError())
$result->ThrowJSONError();
else
JSON_ReturnData(["success" => $result->GetData()]);
}
?>

View File

@@ -1,17 +0,0 @@
{
"db": {
"addr": "localhost",
"name": "e949",
"user": "e949",
"pass": "password"
},
"registration": {
"active": true,
"need_email": false,
"need_invite": false,
"allowed_syms": "a-zA-Z0-9_=+-"
},
"accounts": {
"external_avatars": false
}
}

51
config.json.example Normal file
View File

@@ -0,0 +1,51 @@
{
"debug": true,
"db": {
"addr": "localhost",
"name": "e949",
"user": "e949",
"pass": "password"
},
"registration": {
"active": true,
"need_email": false,
"need_invite": false,
"allowed_syms": "a-zA-Z0-9_=+-",
"min_passw_len": 8
},
"accounts": {
"external_avatars": false
},
"media": {
"pics_path": "media/pics/",
"prevs_path": "media/prevs/",
"previews_enabled": true,
"max_pic_size": 56623104,
"max_pic_res": {
"x": 8192,
"y": 8192,
"ratio": 20
},
"allowed_exts": [
"jpg",
"jpeg",
"png"
],
"allowed_mimetypes": [
"image/jpeg",
"image/png"
]
},
"posting": {
"tags": {
"max_per_post": 256,
"max_single_length": 256,
"max_raw_input_str_length": 1536,
"allowed_syms": "a-zA-Z0-9_"
},
"title": {
"max_length": 4096
}
},
"max_posts_per_request": 120
}

View File

@@ -18,6 +18,7 @@ Files starting from "_" ("_example.php") are intended for internal use only.
- _auth.php: things related to authentification
- _errors.php: error strings
- _json.php: wrappers for JSON functions
- _utils.php: random utility functions
- [ ] stats.php (GET/POST): all general statistics about this instance
@@ -29,7 +30,7 @@ Files starting from "_" ("_example.php") are intended for internal use only.
- [ ] user/list.php (GET/POST): get list of all users
- [ ] user/create.php (POST): create new user account
- [ ] user/edit.php (POST): edit user profile
- [ ] user/delete.php (POST): delete user account
- [x] user/delete.php (POST): delete user account
- [ ] post/ (GET/POST): get single post by id
- [ ] post/search.php (GET/POST): get list of posts matching the criteria
@@ -41,4 +42,4 @@ Files starting from "_" ("_example.php") are intended for internal use only.
- [ ] comments/ (GET/POST): show all comments from section by id
- [ ] comments/create.php (POST): create new comment at selected section
- [ ] comments/edit.php (POST): edit existing comment
- [ ] comments/delete.php (POST): remove existing comment
- [ ] comments/delete.php (POST): remove existing comment

View File

@@ -4,10 +4,14 @@
We are using MariaDB, but any MySQL-compatible database should be enough. There are instructions how to setup it for using with E949.
1. Login to your SQL database with admin account:
```bash
mysql -u root -p
```
2. Setup new user and database:
```mysql
CREATE USER e949@localhost IDENTIFIED BY 'password';
CREATE DATABASE e949 CHARACTER SET = 'utf8';
@@ -16,17 +20,21 @@ FLUSH PRIVILEGES;
EXIT
```
3. Login with new account:
```bash
mysql -u e949 -p
```
4. Create tables:
```mysql
USE e949;
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'Unique identifier of user',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When account was created',
login VARCHAR(255) NOT NULL COMMENT 'User login',
email VARCHAR(255) NULL COMMENT 'User e-mail address',
login VARCHAR(255) NOT NULL UNIQUE COMMENT 'User login',
email VARCHAR(255) NULL UNIQUE COMMENT 'User e-mail address',
password_hash BINARY(32) NOT NULL COMMENT 'User password hash',
salt VARCHAR(8) NOT NULL COMMENT 'User salt, used for password hash',
avatar_path VARCHAR(255) NULL COMMENT 'Path or URL to avatar picture',
@@ -69,4 +77,11 @@ CREATE TABLE invites (
author_id INT UNSIGNED NULL COMMENT 'ID of user, who created invite',
uses_last SMALLINT UNSIGNED NOT NULL COMMENT 'Remaining uses of invite'
);
```
CREATE TABLE approved_tags (
value VARCHAR(255) NOT NULL UNIQUE COMMENT 'The tag itself',
author_id INT UNSIGNED NULL COMMENT 'ID of user who added this tag to list of approved',
added_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When this tag was added'
);
```
5. Profit!!!

View File

@@ -2,5 +2,4 @@
**api**: E949 PHP API
**docs**: all documentation here
<!--**html_drafts**: peaces of html markup-->
**front**: things for frontend, like CSS
**front**: things for frontend, like CSS, php script pieces and markup drafts

View File

@@ -3,4 +3,4 @@
Restrict access to files:
- Any file from `api` directory with prepended `_`
- `config.json`
- `config.json`

3
front/favicon.html Normal file
View File

@@ -0,0 +1,3 @@
<link rel="shortcut icon" href="front/images/favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="32x32" href="front/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="front/images/favicon-16x16.png">

16
front/footer.php Normal file
View File

@@ -0,0 +1,16 @@
<div class="visualbox footer">
<div class="quicklinks">
<p>
<a title="Contacts" href="./?do=show_contacts">Contacts</a> |
<a title="Terms of service" href="./?do=show_tos">Terms of service</a> |
<a title="Privacy policy" href="./?do=there_are_my_data">Privacy</a>
</p>
</div>
<div class="description">
<p>
E949: The newest generation imageboard.<br>
Copyright (C) 2023 Cyclone-Team<br>
Yes, this project is <a title="E949 source code repository" href="https://git.projectsegfau.lt/Cyclone-Team/e949">open source</a><br>
</p>
</div>
</div>

25
front/head.php Normal file
View File

@@ -0,0 +1,25 @@
<meta charset="UTF-8">
<?php
// <head> ... </head>
if (!isset($PAGE_TITLE)) {
http_response_code(500);
die("\$PAGE_TITLE not set");
}
echo "<title>E949: $PAGE_TITLE</title>\n";
require_once("favicon.html");
?>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="front/styles/base.css">
<link rel="stylesheet" href="front/styles/footer.css">
<?php
// Include custom page style, if exists
if (isset($PAGE_STYLE)) {
echo "<link rel=\"stylesheet\" href=\"$PAGE_STYLE\">";
}
?>

39
front/notifications.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
// Notifications
// Notices queue
$NTFY_NoticesQueue = array();
// Add new notice with selected type
function NTFY_AddNotice (string $text, string $type = "fail") {
global $NTFY_NoticesQueue;
switch ($type) {
case "fail":
$NTFY_NoticesQueue[] = "<div class=\"notification_fail\"><p>$text</p></div>";
break;
case "warning":
$NTFY_NoticesQueue[] = "<div class=\"notification_warning\"><p>$text</p></div>";
break;
case "success":
$NTFY_NoticesQueue[] = "<div class=\"notification_success\"><p>$text</p></div>";
break;
default:
die("invalid notification type: $type");
}
}
// Echo all notifications
function NTFY_EchoAllNotices () {
global $NTFY_NoticesQueue;
foreach ($NTFY_NoticesQueue as $notice) {
echo "$notice\n";
}
}
?>

View File

@@ -0,0 +1,34 @@
<?php
// Main page posts counter
if (!$IS_FRONTEND) {
http_response_code(500);
die();
}
// Includes
require_once("api/post/index.php");
$totalPostsAmount = Post_GetPostsAmount();
$totalPostsAmount = strval($totalPostsAmount);
$totalPostsAmountLen = strlen($totalPostsAmount);
?>
<div class="visualbox">
<?php
// India stronk 🇮🇳💪
$allNumbers = array();
while (count($allNumbers) < (7 - $totalPostsAmountLen))
$allNumbers[] = "<img src=\"front/images/counter/0.png\">";
for ($i = 0; $i < $totalPostsAmountLen; ++$i)
$allNumbers[] = "<img src=\"front/images/counter/" . $totalPostsAmount[$i] . ".png\">";
foreach ($allNumbers as $numberImg)
echo $numberImg;
?>
</div>

View File

@@ -0,0 +1,5 @@
<?php
require_once("front/pages/index/random_meme.php");
require_once("front/pages/index/searchbox.php");
require_once("front/pages/index/counter.php");
?>

View File

@@ -0,0 +1,6 @@
<?php
// TODO: picking random meme
?>
<div class="visualbox">
<img src="test.png">
</div>

View File

@@ -0,0 +1,57 @@
<?php
// Main page search box
if (!$IS_FRONTEND) {
http_response_code(500);
die();
}
// Includes
require_once("api/user/index.php");
?>
<div class="visualbox searchbox">
<a class="title" href=".">E949</a>
<div class="nav">
<?php
// If user is logged in
if ($LOGGED_IN) {
// Showing user profile button
$res = User_GetInfoByID($THIS_USER);
if ($res->IsError())
$res->ThrowJSONError();
$uname = $res->GetData()["login"];
echo "<a class=\"useraccount\" title=\"Account page\" href=\"./?do=user_info&id=$THIS_USER\">$uname</a>\n";
unset($res);
// Showing post creation button
echo "<a title=\"Create new post\" href=\"./?do=new_post\">New post</a>\n";
} else { // If user is NOT logged in
?>
<a title="Login in existing account" href="./?do=login">Login</a>
<a title="Create new account" href="./?do=register">Register</a>
<?php
}
?>
<a title="A paginated list of every post" href="./?do=search_posts">Posts</a>
<a title="A paginated list of every tag" href="./?do=view_tags">Tags</a>
<a title="Statistics of current instance" href="./?do=view_stats">Statistics</a>
<a title="A site map" href="./?do=view_sitemap">Site map</a>
</div>
<div>
<form action="." accept-charset="UTF-8" method="get">
<input type="hidden" name="do" value="search_posts">
<input type="text" name="query" value="" size="36" autofocus="autofocus" autocomplete="on"><br>
<input type="submit" value="Search">
<!-- TODO: JS
<input type="button" value="Show random meme" id="random-meme">
-->
</form>
</div>
</div>

View File

@@ -0,0 +1,55 @@
<?php
// Login page
// Includes
require_once("api/user/login.php");
require_once("front/pages/main_nav.php");
require_once("front/notifications.php");
// Redirecting to main page if already logged in
if ($LOGGED_IN) {
header("Location: .");
exit();
}
// If there is attempt to login in
if (isset($_POST["login"]) || isset($_POST["password"])) {
$result = User_Login_Method($_POST);
if ($result->IsError()) { // Something happened
NTFY_AddNotice("Failed to log into account! Check your credentials and try again.<br>" . $result->GetError());
} else { // All OK
header("Location: .");
exit();
}
}
NTFY_EchoAllNotices();
?>
<div class="visualbox">
<h1>Login</h1>
<form class="basicform" action="./?do=login" accept-charset="UTF-8" method="post">
<div>
<label for="login">Username</label><br>
<input type="text" name="login" id="login" <?php if (isset($_POST["login"])) { echo "value=\"" . $_POST["login"] . "\""; } ?>>
</div>
<div>
<label for="password">Password</label><br>
<input type="password" name="password" id="password">
<a href="./?do=reset_password">Reset</a>
</div>
<div>
<input type="submit" value="Submit">
</div>
</form>
<div class="loginmisc">
<p>Don&#39;t have an account? <a href="./?do=register">Register here</a></p>
</div>
</div>

62
front/pages/main_nav.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
// Main navigation bar
?>
<nav class="main">
<ul>
<li>
<p><a title="Main site page" href="./?do=main">Main</p></a>
</li>
<span>|</span>
<?php
if ($LOGGED_IN) {
// Getting user nickname
$res = User_GetInfoByID($THIS_USER);
if ($res->IsError())
$res->ThrowJSONError();
$uname = $res->GetData()["login"];
?>
<li>
<p><a title="User page" href="./?do=user_info&amp;id=<?php echo $THIS_USER; ?>">Me (<?php echo $uname; ?>)</p></a>
</li>
<span>|</span>
<li>
<p><a title="Create new post" href="./?do=new_post">New post</p></a>
</li>
<?php
} else {
?>
<li>
<p><a title="Login in existing account" href="./?do=login">Login</p></a>
</li>
<span>|</span>
<li>
<p><a title="Create new account" href="./?do=register">Register</p></a>
</li>
<?php
}
?>
<span>|</span>
<li>
<p><a title="A paginated list of every post" href="./?do=search_posts">Posts</p></a>
</li>
<span>|</span>
<li>
<p><a title="A (not yet) paginated list of every tag" href="./?do=view_tags">Tags</p></a>
</li>
<span>|</span>
<li>
<p><a title="Statistics of current instance" href="./?do=view_stats">Stats</p></a>
</li>
<span>|</span>
<li>
<p><a title="Contacts" href="./?do=show_contacts">Contacts</p></a>
</li>
<span>|</span>
<li>
<p><a title="A site map" href="./?do=view_sitemap">Site map</p></a>
</li>
</ul>
</nav>

View File

@@ -0,0 +1,62 @@
<?php
// Post creation page
// Includes
require_once("api/post/create.php");
// Markup includes
require_once("front/pages/main_nav.php");
require_once("front/notifications.php");
// Redirecting to main page if not logged in
if (!$LOGGED_IN) {
header("Location: .");
exit();
}
// Processing POST-request
if (isset($_POST) && $_POST) {
if (isset($_POST["tags"]) && $_POST["tags"] && isset($_FILES["pic"])) {
if (isset($_POST["title"]) && !$_POST["title"])
unset($_POST["title"]);
$result = Post_Create_Method($_POST, $_FILES);
if ($result->IsError()) { // Something happened
NTFY_AddNotice("Failed to create post! Reason:<br>" . $result->GetError());
} else { // All OK
header("Location: ./?do=view_post&id=" + strval($result->GetData()));
exit();
}
} else {
NTFY_AddNotice("You must supply image and tags for post");
}
}
NTFY_EchoAllNotices();
?>
<div class="visualbox">
<h1>New post</h1>
<form class="basicform" action="./?do=new_post" accept-charset="UTF-8" method="post" enctype="multipart/form-data">
<div>
<label for="pic">Select image:</label><br>
<input type="file" name="pic" id="pic">
</div>
<div>
<label for="tags">Comma-separated tags list:</label><br>
<textarea placeholder="tag_1, tag_2, tag_3, ..., tag_N" name="tags" id="tags" style="width: 98%;" rows="1"><?php if (isset($_POST["tags"])) { echo $_POST["tags"]; } ?></textarea>
</div>
<div>
<label for="title">Post title:</label><br>
<textarea placeholder="Lorem ipsum dolor sit amet..." name="title" id="title" style="width: 98%;" rows="2"><?php if (isset($_POST["title"]) && $_POST["title"]) { echo $_POST["title"]; } ?></textarea>
</div>
<div>
<input type="submit" value="Submit">
</div>
</form>
</div>

View File

@@ -0,0 +1,99 @@
<?php
// Registration page
// Includes
require_once("api/_config.php");
require_once("api/user/create.php");
require_once("front/pages/main_nav.php");
require_once("front/notifications.php");
$REGISTRATION_IS_OPEN = $Config["registration"]["active"];
// Redirecting to main page if already logged in
if ($LOGGED_IN) {
header("Location: .");
exit();
}
// If there is attempt to register
if ((isset($_POST["login"]) || isset($_POST["password"])) && $REGISTRATION_IS_OPEN) {
// If ToS arent accepted
if ($_POST["tos_check"] !== "tos_check") {
NTFY_AddNotice("You MUST accept Terms of Service!", "fail");
} else { // Ok, noice
if ($_POST["password"] === $_POST["password2"]) {
$result = User_Create_Method($_POST);
if ($result->IsError()) { // Something happened
NTFY_AddNotice("Failed to create account! Reason:<br>" . $result->GetError(), "fail");
} else { // All OK
NTFY_AddNotice("Account registered! You can log in <a href=\"./?do=login\">here</a>", "success");
}
} else {
NTFY_AddNotice("Password mismatch! Check and try again", "fail");
}
}
}
NTFY_EchoAllNotices();
?>
<div class="visualbox">
<h1>Register</h1>
<?php
if ($REGISTRATION_IS_OPEN) {
?>
<form class="basicform" action="./?do=register" accept-charset="UTF-8" method="post">
<div>
<label for="login">Your desired username:</label><br>
<input type="text" name="login" id="login" spellcheck="false" <?php if (isset($_POST["login"])) { echo "value=\"" . $_POST["login"] . "\""; } ?>>
</div>
<div>
<label for="password">Password:</label><br>
<input type="password" name="password" id="password"><br>
<label for="password2">Repeat password:</label><br>
<input type="password" name="password2" id="password2">
</div>
<?php
if ($Config["registration"]["need_email"]) { // TODO: move description to ToS
?>
<div>
<label for="email">E-Mail address <span style="font-size: 50%;">(we will send you penis enlargement ads)</span>:</label><br>
<input type="text" name="email" id="email" inputmode="email" <?php if (isset($_POST["email"])) { echo "value=\"" . $_POST["email"] . "\""; } ?>>
</div>
<?php
}
if ($Config["registration"]["need_invite"]) {
?>
<div>
<label for="invite_id">Invite code:</label><br>
<input type="text" name="invite_id" id="invite_id" spellcheck="false" <?php if (isset($_POST["invite_id"])) { echo "value=\"" . $_POST["invite_id"] . "\""; } ?>>
</div>
<?php
}
?>
<div>
<input type="checkbox" name="tos_check" id="tos_check" value="tos_check">
<label for="tos_check">I've read and accept your boring Terms of Service</label>
</div>
<div>
<input type="submit" value="Submit">
</div>
</form>
<div class="loginmisc">
<p>Already have an account? <a href="./?do=login">Login here</a></p>
</div>
<?php
} else {
echo "<h2 style=\"color: red;\">Registrations are closed!</h2>";
}
?>
</div>

View File

@@ -0,0 +1,58 @@
<?php
// Create entry in posts search list
// Includes
require_once("api/user/index.php");
// Create entry from post info
function GenPostEntry (array $data): string {
$userReqResp = User_GetInfoByID($data["author_id"]);
if ($userReqResp->IsError())
$userLogin = strval($data["author_id"]);
else
$userLogin = $userReqResp->GetData()["login"];
if (!is_string($data["created_at"]))
$timestamp = strval($data["created_at"]);
else
$timestamp = $data["created_at"];
$placeholderString = "ID: " . strval($data["id"]);
$placeholderString .= " | AUTHOR: $userLogin";
$placeholderString .= " | TIMESTAMP: $timestamp";
$placeholderString .= " | TAGS: " . $data["tags"];
$result = "<a class=\"entry\">\n"; // TODO: href
$result .= "<img src=\"";
if ($data["preview_path"])
$result .= $data["preview_path"];
else
$result .= $data["pic_path"];
$result .= "\" alt=\"$placeholderString\" title=\"$placeholderString\">\n";
$result .= "<div class=\"stats\">\n";
$result .= "<p><b>+</b>" . strval($data["votes_up"]);
$result .= " <b>-</b>" . strval($data["votes_down"]);
$result .= " <b>V</b> " . strval($data["views"]);
/*
if ($data["comments_enabled"]) {
$result .= " <b>C</b> " . null; // TODO: resolve comment section by id and count comments amount
}
*/
$result .= "</p>\n";
$result .= "</div>\n";
$result .= "</a>\n";
return $result;
}
?>

View File

@@ -0,0 +1,179 @@
<?php
// Search posts by filter
// Includes
// API
require_once("api/_input_checks.php");
require_once("api/post/find.php");
// Front pieces
require_once("front/pages/search_posts/gen_post_entry.php");
// Markup includes
require_once("front/pages/main_nav.php");
require_once("front/notifications.php");
// Needed config values
$cfgMaxPostsPerRequest = $Config["max_posts_per_request"];
// Checking request fields
// Checking amount of posts per page
$postsPerPage = null;
if (isset($_REQUEST["amount"])) {
$postsPerPage = $_REQUEST["amount"];
if (
(!InpChk_IsValidInt32($postsPerPage))
|| ($postsPerPage > $cfgMaxPostsPerRequest)
|| ($postsPerPage < 1)
) {
$postsPerPage = $_REQUEST["amount"] = $cfgMaxPostsPerRequest; // TODO: user defaults
NTFY_AddNotice("Wrong posts \"amount\" value in query, was normalized to $postsPerPage.", "warning");
} else {
$_REQUEST["amount"] = intval($_REQUEST["amount"]);
}
} else {
$postsPerPage = $cfgMaxPostsPerRequest; // TODO: user defaults
}
// Checking posts offset
$currentOffset = null;
if (!empty($_REQUEST["offset"])) {
$currentOffset = $_REQUEST["offset"];
if (
(!InpChk_IsValidInt32($currentOffset))
|| $currentOffset < 0
) {
$currentOffset = $_REQUEST["offset"] = 0;
NTFY_AddNotice("Wrong \"offset\" value in query, was defaulted to $currentOffset.", "warning");
} else {
$_REQUEST["offset"] = intval($_REQUEST["offset"]);
}
} else {
$currentOffset = 0;
}
$currentOffsetIsCorrect = ($currentOffset === 0) || (!($currentOffset % $postsPerPage));
// Processing request
$result = Post_GetMatchingPosts_Method($_REQUEST);
$requestedPostsResult = null;
if ($result->IsError()) // Something happened
NTFY_AddNotice("Failed to fetch posts! Reason:<br>" . $result->GetError(), "fail");
else
$requestedPostsResult = $result->GetData();
// Checking offset again NOTICE: doshirak-code
if ($requestedPostsResult && $currentOffset > $requestedPostsResult["total_amount"]) {
$currentOffset = $_REQUEST["offset"] = 0;
NTFY_AddNotice("Wrong \"offset\" value in query!", "fail");
$requestedPostsResult = null;
}
NTFY_EchoAllNotices();
?>
<div class="visualbox">
<menu class="paginator">
<?php
// TODO: move to function, remove trashy dependencies and dublicate at bottom of posts list
if ($requestedPostsResult) {
// Calculating current page based on offset NOTICE: counting starts from zero, so user will see $currentPage + 1
$currentPage = intdiv($currentOffset, $postsPerPage);
if (!$currentOffsetIsCorrect)
++$currentPage;
// Calculating maximum page based on total posts amount
$totalPagesAmount = intdiv($requestedPostsResult["total_amount"], $postsPerPage);
if ($requestedPostsResult["total_amount"] % $postsPerPage)
++$totalPagesAmount;
} else {
$currentPage = 0;
$totalPagesAmount = 0;
}
$tempGETArr = $_GET;
$tempGETArr["offset"] = $currentOffset; // Starting offset
$startingPage = $currentPage; // And page, yeah
// Calculating starting offset
for ($i = 0; $i < 2 && $tempGETArr["offset"] > 0; ++$i) {
$tempGETArr["offset"] -= $postsPerPage;
--$startingPage;
}
// If list of posts does not begin from first
if ($currentOffset > 0) { // Then adding link to first and previous page
$secondTempGETArr = $_GET;
$secondTempGETArr["offset"] = 0;
// First page
echo "<li><a title=\"First page\" href=\"./?" . http_build_query($secondTempGETArr) . "\">First</a></li>\n";
// Previous page
if ($currentOffsetIsCorrect) {
$secondTempGETArr["offset"] = $currentOffset - $postsPerPage;
echo "<li><a title=\"Previous page\" href=\"./?" . http_build_query($secondTempGETArr) . "\">&lt;</a></li>\n";
}
unset($secondTempGETArr);
echo "<li><p>...</p></li>";
}
// Showing pages
$i = $startingPage;
$endingPage = $startingPage + 5; // Unaccurate naming, tbh, but who cares
for (; $i < $endingPage && $i < $totalPagesAmount; ++$i) {
echo "<li>";
if ($tempGETArr["offset"] === $currentOffset)
echo "<b>";
echo "<a href=\"./?" . http_build_query($tempGETArr) . "\">" . strval($i + 1) ."</a>";
if ($tempGETArr["offset"] === $currentOffset)
echo "</b>";
echo "</li>\n";
$tempGETArr["offset"] += $postsPerPage;
}
// If we have even MORE posts!
if ($currentPage < ($totalPagesAmount - 1)) { // Then adding link to last and next page
echo "<li><p>...</p></li>";
$secondTempGETArr = $_GET;
// Next page
if ($currentOffsetIsCorrect) {
$secondTempGETArr["offset"] = $currentOffset + $postsPerPage;
echo "<li><a title=\"Next page\" href=\"./?" . http_build_query($secondTempGETArr) . "\">&gt;</a></li>\n";
}
// Last page
$secondTempGETArr["offset"] = ($totalPagesAmount - 1) * $postsPerPage;
echo "<li><a title=\"Last page\" href=\"./?" . http_build_query($secondTempGETArr) . "\">Last</a></li>\n";
}
?>
</menu>
<div class="postsearchcolumn">
<h3 style="margin-top: 4px; margin-bottom: 4px;">Search</h3>
<form class="basicform" action="." accept-charset="UTF-8" method="get">
<input type="hidden" name="do" value="search_posts">
<input type="text" name="query" autocomplete="on" <?php if (isset($_REQUEST["query"])) { echo "value=\"" . $_REQUEST["query"] . "\""; } ?>>
<input type="submit" value="Search">
</form>
</div>
<div class="postlist">
<?php
if ($requestedPostsResult && $requestedPostsResult["total_amount"]) {
foreach ($requestedPostsResult["data"] as $postData)
echo GenPostEntry($postData);
} else {
echo "<h2 style=\"color: gray; font-style: italic;\">Nothing found!</h2>";
}
?>
</div>
</div>

View File

@@ -0,0 +1,50 @@
<?php
// Approved tags list
// Includes
require_once("api/_config.php");
require_once("api/user/index.php");
require_once("api/tags/index.php");
require_once("api/post/index.php");
require_once("api/comments/index.php");
require_once("front/pages/main_nav.php");
require_once("front/notifications.php");
// Getting stats
$statUsers = User_GetUsersAmount(); // array
$statTags = Tags_GetTagsAmount(); // int
$statPosts = Post_GetPostsAmount(); // int
$statComms = Comments_GetTotalAmount(); // int
NTFY_EchoAllNotices();
?>
<div class="visualbox">
<h1>Instance statistics</h1>
<table>
<tr>
<th><p>Users amount</p></th>
<td><p>Total: <?php echo $statUsers["users"]; ?></p></td>
<td><p>Banned: <?php echo $statUsers["banned"]; ?></p></td>
</tr>
<tr>
<th><p>Tags amount</p></th>
<td><p>Total: <?php echo $statTags; ?></p></td>
</tr>
<tr>
<th><p>Posts amount</p></th>
<td><p>Total: <?php echo $statPosts; ?></p></td>
</tr>
<tr>
<th><p>Comments amount</p></th>
<td><p>Total: <?php echo $statComms; ?></p></td>
</tr>
</table>
</div>

View File

@@ -0,0 +1,62 @@
<?php
// Approved tags list
// Includes
require_once("api/_config.php");
require_once("api/user/index.php");
require_once("api/tags/index.php");
require_once("front/pages/main_nav.php");
require_once("front/notifications.php");
$TAGS_NEED_TO_BE_FILTERED = false;
if (isset($_GET["s"])) {
if (strlen($_GET["s"]) > 0 && strlen($_GET["s"]) < $Config["posting"]["tags"]["max_single_length"])
$TAGS_NEED_TO_BE_FILTERED = true;
}
$result = Tags_GetListOfApproved_Method();
if ($result->IsError())
NTFY_AddNotice("Error occured while trying to get tags list:<br>" . $result->GetError(), "fail");
NTFY_EchoAllNotices();
?>
<div class="visualbox">
<h1>Approved tags list</h1>
<?php
if ($TAGS_NEED_TO_BE_FILTERED)
echo "<p style=\"margin-bottom: 15px;\"><b>Search term:</b> " . $_GET["s"] . "</p>\n";
$cachedAuthorsInfo = array();
if (!$result->GetData()) {
echo "<h2 style=\"color: gray; font-style: italic;\">Nothing here yet</h2>";
} else {
foreach ($result->GetData() as $tagInfo) {
if ($TAGS_NEED_TO_BE_FILTERED && !str_contains($tagInfo["value"], $_GET["s"]))
continue;
echo "<div style=\"display: inline-block; margin-right: 20px; margin-bottom: 10px;\">\n";
echo "<p>" . $tagInfo["value"] . "</p>\n";
echo "<p style=\"font-size: 70%\">Added by: ";
$aid = $tagInfo["author_id"];
if (!isset($cachedAuthorsInfo[$aid])) {
$uinfo = User_GetInfoByID($aid);
if ($uinfo->IsError()) { // Seems like no such user id
$cachedAuthorsInfo[$aid] = strval($aid) . " <i>(user deleted)</i>";
} else {
$cachedAuthorsInfo[$aid] = "<a href=\"./?do=user_info&id=" . strval($aid) . "\">" . $uinfo->GetData()["login"] . "</a>";
}
}
echo $cachedAuthorsInfo[$aid];
echo "</p>\n</div>\n";
}
}
?>
</div>

View File

@@ -0,0 +1,39 @@
<?php
// Approved tags list
// Includes
// API
require_once("api/_config.php");
require_once("api/user/index.php");
// Front pieces
require_once("front/pages/main_nav.php");
require_once("front/notifications.php");
// Process request
if (isset($_REQUEST["id"])) {
$result = User_GetInfoByID_Method($_REQUEST);
if ($result->IsError())
NTFY_AddNotice("Failed to fetch posts! Reason:<br>" . $result->GetError());
} else {
header("Location: .");
exit();
}
NTFY_EchoAllNotices();
?>
<div class="visualbox">
<?php
if (!$result->IsError()) {
echo "<h2>" . $result["login"] . "'s personal page</h2>";
// TODO
}
?>
</div>

View File

@@ -0,0 +1,106 @@
<?php
// View post by ID
// Includes
// API
require_once("api/post/index.php");
require_once("api/comments/index.php");
// Markup includes
require_once("front/pages/main_nav.php");
require_once("front/notifications.php");
// Processing request
$reqResult = Post_GetByID_Method($_GET);
$postData = null;
if ($reqResult->IsError()) { // Something happened, very likely that post is not found
header("Location: .");
exit();
} else {
$postData = $reqResult->GetData();
}
NTFY_EchoAllNotices();
?>
<div class="visualbox">
<div class="postviewer">
<div class="stats">
<div title="Positive votes">
<img src="front/images/plus.png" alt="Positive votes icon">
<p><?php echo $postData["votes_up"]; ?></p>
</div>
<div title="Negative votes">
<img src="front/images/minus.png" alt="Negative votes icon">
<p><?php echo $postData["votes_down"]; ?></p>
</div>
<div title="Views count">
<img src="front/images/eye.png" alt="Views count icon">
<p><?php echo $postData["views"]; ?></p>
</div>
<!--TODO: reset button for moderators-->
</div>
<div class="picture">
<?php
echo "<img src=\"" . $postData["pic_path"] . "\" alt=\"" . $postData["tags"] . "\" title=\"" . $postData["tags"] . "\">\n";
?>
</div>
<div class="tags">
<ul>
<?php
$tagsArr = explode(",", $postData["tags"]);
foreach ($tagsArr as $tag)
echo "<li>$tag</li>\n";
?>
</ul>
</div>
<div class="comments">
<?php
if (!$postData["comments_enabled"]) {
echo "<p style=\"color: gray;\"><i>Comments disabled</i></p>\n";
} else { // TODO: this part down is untested (and incomplete), so beware
$reqResult = Comments_GetSectionRange_Method(array("id" => $postData["comment_section_id"]));
if ($reqResult->IsError()) {
echo "<p style=\"color: gray;\"><i>Can't fetch comments</i></p>\n";
} else {
$commentsList = $reqResult->GetData();
$commentsAmount = count($commentsList);
echo "<p>Comments: " . strval($commentsAmount) . "</p>\n";
foreach ($commentsList as $commentData) { // TODO
echo "<div class=\"entry\">\n";
echo "<div class=\"meta\">\n";
echo "<img src=\"front/images/pfp_placeholder.png\" alt=\"Guy's pfp\">\n";
echo "<p>\n";
echo strval($commentData["created_at"]) . "<br>\n"; //"04/04/2024 04:20<br>\n";
echo "<a href=\"./noway\">Guy</a>\n";
echo "</p>\n";
echo "</div>\n";
echo "<p>cool story bob</p>\n";
echo "</div>\n";
}
}
}
?>
<!--
<p>Comments: 54</p>
<div class="entry">
<div class="meta">
<img src="front/images/pfp_placeholder.png" alt="Guy's pfp">
<p>
04/04/2024 04:20<br>
<a href="./noway">Guy</a>
</p>
</div>
<p>cool story bob</p>
</div>
-->
</div>
</div>
</div>

184
front/styles/base.css Normal file
View File

@@ -0,0 +1,184 @@
/* The most base style, used everywhere */
/* Common */
*:focus {
outline: 2px dotted #49f49f;
}
body {
background-color: #094e59;
background-image: url("../images/bg_pattern_peace.png");
background-repeat: repeat;
}
body, div, p, a {
padding: 0;
margin: 0;
}
/* Fonts */
h1, h2, h3, h4, h5, h6, p, ul, li, dd, dt, label {
font-family: Verdana, Sans-Serif;
}
h1, h2, h3, h4, h5, h6, p, a, label {
color: #00c07c;
text-decoration: none;
}
a {
color: #009049;
text-decoration: underline;
transition: all 0.2s;
}
a:hover {
color: #00c07c;
cursor: pointer;
}
/* Custom blocks */
div.wrapper {
margin: auto;
}
div.visualbox {
margin: 10px;
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 5px #000;
text-shadow: 0 0 2px black, 0 0 6px black;
backdrop-filter: blur(6px);
}
/* Notifications */
div.notification_fail {
margin: 10px;
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 5px red;
text-shadow: 0 0 2px black, 0 0 6px black;
backdrop-filter: blur(6px);
background-color: #f003;
}
div.notification_fail p {
color: red;
}
div.notification_warning {
margin: 10px;
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 5px orange;
text-shadow: 0 0 2px black, 0 0 6px black;
backdrop-filter: blur(6px);
background-color: #fa03;
}
div.notification_warning p {
color: orange;
}
div.notification_success {
margin: 10px;
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 5px green;
text-shadow: 0 0 2px black, 0 0 6px black;
backdrop-filter: blur(6px);
background-color: #0f03;
}
/* Input */
/* Text input */
input[type=text], input[type=password], textarea {
color: #00c07c;
background-color: transparent;
border: 2px solid #009049;
border-radius: 3px;
font-family: Verdana, Sans-Serif;
font-size: 16px;
text-shadow: 0 0 6px black;
transition: all 0.25s;
}
input[type=text]:hover, input[type=password]:hover, textarea:hover {
border: 2px solid transparent;
border-bottom: 2px solid #009049;
}
input[type=text]:focus, input[type=password]:focus, textarea:focus {
border: 2px solid transparent;
border-bottom: 2px solid #49f49f;
outline: none;
}
/* Submit button */
input[type=submit] {
background-color: transparent;
border: 2px solid #009049;
border-radius: 3px;
color: #00c07c;
text-shadow: 0 0 6px #000a;
font-family: Verdana, Sans-Serif;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
input[type=submit]:hover {
border: 2px solid transparent;
border-bottom: 2px solid #009049;
background-color: #009049a0;
color: #49f49f;
}
input[type=submit]:focus {
border: 2px solid #49f49f;
outline: none;
}
/* Checkbox */
input[type=checkbox] {
cursor: pointer;
}
/* File picker */
input[type=file] {
color: #00c07c;
text-shadow: 0 0 6px #000a;
font-family: Verdana, Sans-Serif;
font-size: 16px;
margin: 4px 0;
}
input[type=file]::file-selector-button {
background-color: transparent;
border: 2px solid #009049;
border-radius: 3px;
color: #00c07c;
text-shadow: 0 0 6px #000a;
font-family: Verdana, Sans-Serif;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
input[type=file]::file-selector-button:hover {
border: 2px solid transparent;
border-bottom: 2px solid #009049;
background-color: #009049a0;
color: #49f49f;
}
input[type=file]::file-selector-button:focus {
border: 2px solid #49f49f;
outline: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 B

View File

@@ -1,70 +0,0 @@
body, div, h1, h2, h3, h4, h5, h6, p, ul, li, dd, dt {
font-family: Verdana, Sans-Serif;
}
body, div, p, a {
padding: 0;
margin: 0;
}
h1, h2, h3, h4, h5, h6, p, a {
color: #00c07c;
text-decoration: none;
}
a {
color: #009049;
}
body {
background-color: #094e59;
background-image: url("./bg_pattern_peace.png");
background-repeat: repeat;
}
div.wrapper {
text-align: center;
margin: auto;
}
/*div.wrapper img {
width: 60%;
height: 60%;
}*/
h1.title {
font-size: 4em;
padding: 0;
margin: 0;
}
div.nav {
margin-bottom: 0.25rem;
}
div.nav > a {
padding: 0.25rem 0.25rem;
}
div.nibbabox {
margin: 10px auto;
padding: 2px 0;
width: 480px;
max-width: 98vw;
border-radius: 5px;
box-shadow: 0 0 5px #000;
text-shadow: 0 0 2px black, 0 0 6px black;
}
div.searchbox {
padding-bottom: 5px;
}
div.notsearchbox {
font-size: 80%;
}
@media only screen and (max-height: 600px) {
div.wrapper {
top: 25vh;
}
}

11
front/styles/footer.css Normal file
View File

@@ -0,0 +1,11 @@
/* Style specifically for footer */
div.footer div.quicklinks {
padding: 0 0 4px 0;
}
div.footer div.description {
font-size: 80%;
}

65
front/styles/index.css Normal file
View File

@@ -0,0 +1,65 @@
/* Stylesheet for index page */
/* Custom wrapper */
div.wrapper {
text-align: center;
width: 65%;
}
/* Index fonts */
div.searchbox a.title {
font-size: 4em;
font-weight: bold;
text-decoration: none;
padding: 0;
margin: 0;
}
div.searchbox a.title:hover {
color: #009049;
}
/* Index search box */
div.searchbox input[type=text] {
margin-top: 2px;
margin-bottom: 4px;
}
div.searchbox input[type=submit] {
margin-top: 4px;
padding: 5px 10px;
}
/* Index navigation */
div.nav {
margin-bottom: 0.25rem;
}
div.nav > a {
padding: 0.25rem 0.25rem;
}
div.nav a.useraccount {
color: orange;
text-decoration: underline;
}
/* Index random meme box */
div.visualbox img {
max-width: 90%;
max-height: 240px;
}

325
front/styles/main.css Normal file
View File

@@ -0,0 +1,325 @@
/* Common stylesheet for most of the site */
/* Adjusting wrapper */
div.wrapper {
padding-top: 26pt;
}
/* Navigation block */
nav.main {
background-color: transparent;
position: fixed;
top: 0;
left: 0;
z-index: 999;
width: 100%;
}
nav.main ul {
position: relative;
margin: 0;
margin-left: 10px;
margin-right: 10px;
padding: 7px;
backdrop-filter: blur(6px);
background-color: #094e5970;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
box-shadow: 0 0 5px #000;
text-shadow: 0 0 2px black, 0 0 6px black;
text-align: center;
display: flex;
flex-flow: row wrap;
justify-content: space-around;
}
nav.main ul li {
display: inline-block;
}
nav.main ul li a {
color: #00c07c;
text-decoration: none;
transition: all 0.2s;
}
nav.main ul li a:hover {
color: #49f49f;
}
nav.main ul li.current a {
font-weight: bold;
text-decoration: underline;
}
nav.main ul span {
color: #00c07c;
padding: 0;
margin: 0;
}
/* Table */
table, tr, th, td {
border: 2px solid #009049;
}
table {
border-collapse: collapse;
}
th, td {
padding: 4px;
}
th {
padding-left: 8px;
padding-right: 8px;
}
td {
padding-left: 16px;
padding-right: 16px;
}
/* Other */
/* Basic form for login and registration */
form.basicform div {
margin-bottom: 14px;
}
form.basicform a {
font-size: 80%;
}
/* Additionals for form */
form.basicform label[for="tos_check"] {
cursor: pointer;
}
div.loginmisc {
margin-top: 4px;
margin-bottom: 4px;
}
div.loginmisc p {
font-style: italic;
font-size: 80%;
}
/* Paginator */
menu.paginator {
text-align: center;
margin: 2px auto;
}
menu.paginator li {
display: inline-block;
margin: 0 8px;
}
menu.paginator li a {
text-decoration: none;
}
menu.paginator li p {
color: gray;
}
/* Post search column styling */
div.postsearchcolumn {
display: block;
float: left;
width: 15em;
}
div.postsearchcolumn form input[type="text"] {
width: 10em;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-right: 1px solid gray;
display: inline-block;
}
div.postsearchcolumn form input[type="submit"] {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-left: 1px solid gray;
display: inline-block;
margin-left: -4px;
}
/* Posts list styling */
div.postlist {
display: block;
margin-left: 15em;
overflow: visible;
padding-left: 1em;
text-align: center;
}
div.postlist a.entry {
display: inline-block;
text-align: center;
vertical-align: text-top;
width: 150px;
max-width: 150px; /* TODO: must be relative or at least tunable through profile settings */
background-color: #00904910;
border-radius: 2px;
box-shadow: 0 0 5px #0006;
transition: all 0.2s;
margin: 5px;
text-decoration: none;
font-size: 10px;
text-shadow: none;
}
div.postlist a.entry:hover {
background-color: #00904920;
box-shadow: 0 0 8px #000a;
scale: 1.015;
}
div.postlist a.entry img {
object-fit: contain;
max-width: 150px;
max-height: 150px; /* TODO: same as stated higher */
}
div.postlist a.entry div.stats {
width: 100%;
border-top: 1px solid #00c07c20;
}
div.postlist a.entry div.stats p {
color: #00c07c;
font-size: 12px;
text-shadow: none;
margin: 3px 0;
transition: all 0.2s;
}
div.postlist a.entry:hover div.stats p {
color: #49f49f;
}
/* Post viewer */
div.postviewer div.stats {
margin-bottom: 0.5em;
}
div.postviewer div.stats * {
display: inline-block;
}
div.postviewer div.stats div {
margin-right: 1em;
}
div.postviewer div.stats img {
width: 1em;
height: 1em;
}
div.postviewer div.picture {
background-color: #aaa3;
border-radius: 3px;
text-align: center;
overflow: clip;
}
div.postviewer div.picture img {
max-width: 100%;
min-width: 50px;
vertical-align: center;
margin-bottom: -4.5px;
}
div.postviewer div.tags {
/* background-color: #aaa3;
background-color: #0002;
border: 2px solid #aaa7;
border-bottom: 2px solid #aaa7;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px; */
}
div.postviewer div.tags ul {
text-align: center;
padding: 0 0 4px 0;
margin: 0;
}
div.postviewer div.tags ul li {
background-color: #009049a0;
color: #00c07c;
text-shadow: 0 0 2px #000c, 0 0 2px #000c;
box-shadow: 0 0 3px #0005;
border-radius: 2px;
padding: 4px;
margin: 4px 0 0 0;
display: inline-block;
transition: all 0.2s;
cursor: default;
}
div.postviewer div.tags ul li:hover {
color: #49f49f;
box-shadow: 0 0 5px #0007;
}
div.postviewer div.comments {}
div.postviewer div.comments p {
margin: 8px 0 8px 0;
}
div.postviewer div.comments div.entry {
margin-top: 8px;
padding: 5px;
border: 1px solid #009049;
border-radius: 3px;
align-items: center;
}
div.postviewer div.comments div.entry p {
display: inline-block;
vertical-align: top;
margin: 0 0 0 0;
}
div.postviewer div.comments div.entry div.meta {
display: inline-block;
width: 12%;
text-align: center;
border-right: 1px solid #009049;
}
div.postviewer div.comments div.entry div.meta img {
display: inline-block;
vertical-align: middle;
width: 64px;
height: 64px;
margin: 0 8px 0 0;
}
div.postviewer div.comments div.entry div.meta p {
display: inline-block;
vertical-align: top;
}

View File

@@ -1,60 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>E949: Index</title>
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png">
<!-- <meta name="theme-color" content="#00549e"> -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--TODO-->
<link rel="stylesheet" href="./front/styles/default.css">
<!-- <script src="./front/scripts/some_script.js" type="text/javascript" integrity="1234"></script> -->
</head>
<body>
<div class="wrapper">
<div class="nibbabox notsearchbox">
<img src="./test.png">
</div>
<div class="nibbabox searchbox">
<h1 class="title"><a href="./">E949</a></h1>
<div class="nav">
<a title="Login in existing account" href="./account/login.php">Login</a>
<a title="Create new account" href="./account/create.php">Signup</a>
<!-- <a title="Account page" href="./account/">Username123</a> -->
<a title="A paginated list of every post" href="./posts/">Posts</a>
<a title="A paginated list of every tag" href="./tags/">Tags</a>
<a title="Statistics of current instance" href="./stats.php">Statistics</a>
<a title="A site map" href="./site_map.php">Site map</a>
</div>
<div>
<form action="./posts" accept-charset="UTF-8" method="get">
<input type="text" name="tags" id="tags" value="" size="30" autofocus="autofocus" data-autocomplete="tag-query"><br>
<input type="submit" value="Search">
<input type="button" value="Change Mascot" id="change-mascot">
</form>
</div>
</div>
<div class="nibbabox notsearchbox">
<img src="./images/counter/3.png">
<img src="./images/counter/4.png">
<img src="./images/counter/5.png">
<img src="./images/counter/1.png">
<img src="./images/counter/9.png">
<img src="./images/counter/5.png">
<img src="./images/counter/3.png">
</div>
<div class="nibbabox notsearchbox">
<p>
Serving 3,451,953 posts<br>
<a title="Takedown Information" href="./static/takedown">Takedown Policy and Process</a> |
<a title="Contact Us" href="./static/contact">Contact Us</a> |
<a title="Advertising with Us" href="./help/advertising">Advertising</a> |
<a title="Terms of Service" href="./static/terms_of_service">Terms of Service</a> |
<a title="Privacy Policy" href="./static/privacy">Privacy</a>
</p>
</div>
</div>
</body>
</html>

99
index.php Normal file
View File

@@ -0,0 +1,99 @@
<?php
// Main page
$IS_FRONTEND = true;
// Includes
require_once("api/_auth.php");
require_once("api/user/index.php");
require_once("front/notifications.php");
$PAGE_TITLE = null; // String that will be showed as "E949: $PAGE_TITLE"
$PAGE_STYLE = null; // Path to file with style that will be included
$PAGE_FILE = null; // Path to main body file that will be included
$PICKED_PAGE = null;
if (isset($_GET["do"]))
$PICKED_PAGE = $_GET["do"];
else
$PICKED_PAGE = "";
// Picking current page
switch ($PICKED_PAGE) {
// Direct-link pages
// Post viewing page
case "view_post":
$PAGE_TITLE = "Post #" . $_GET["id"]; // NOTICE: not good
$PAGE_STYLE = "front/styles/main.css";
$PAGE_FILE = "front/pages/view_post/page.php";
break;
// Available-on-login pages
// Post creation page
case "new_post":
$PAGE_TITLE = "Create new post";
$PAGE_STYLE = "front/styles/main.css";
$PAGE_FILE = "front/pages/new_post/page.php";
break;
// Navigable pages
// Common instance statistics
case "view_stats":
$PAGE_TITLE = "Instance statistics";
$PAGE_STYLE = "front/styles/main.css";
$PAGE_FILE = "front/pages/stats/page.php";
break;
// Approved tags viewer
case "view_tags":
$PAGE_TITLE = "Approved tags list";
$PAGE_STYLE = "front/styles/main.css";
$PAGE_FILE = "front/pages/tags_viewer/page.php";
break;
// Posts viewer
case "search_posts":
$PAGE_TITLE = "Search posts";
$PAGE_STYLE = "front/styles/main.css";
$PAGE_FILE = "front/pages/search_posts/page.php";
break;
// Registration page
case "register":
$PAGE_TITLE = "Register";
$PAGE_STYLE = "front/styles/main.css";
$PAGE_FILE = "front/pages/register/page.php";
break;
// Login page
case "login":
$PAGE_TITLE = "Login";
$PAGE_STYLE = "front/styles/main.css";
$PAGE_FILE = "front/pages/login/page.php";
break;
// Main page
case "index":
case "main":
default:
$PAGE_TITLE = "Index";
$PAGE_STYLE = "front/styles/index.css";
$PAGE_FILE = "front/pages/index/page.php";
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<?php require_once("front/head.php"); ?>
</head>
<body>
<div class="wrapper">
<?php
require_once($PAGE_FILE);
require_once("front/footer.php");
?>
</div>
</body>
</html>

BIN
test.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 246 KiB