Compare commits
19 Commits
41ddbcab9e
...
dev
Author | SHA1 | Date | |
---|---|---|---|
e538a76b09 | |||
62b7b68976 | |||
8700a544b9 | |||
705e8cd6a2 | |||
de456dea0a | |||
751476c4f8 | |||
93a2286d46 | |||
e180c04e44 | |||
c48f837738 | |||
4e1c36d670 | |||
6958b75414 | |||
8d74a51937 | |||
a573faf5a1 | |||
12143c148d | |||
e487ed79c4 | |||
9a4658f3ea | |||
983a5d0353 | |||
ae719995ee | |||
074ce120e9 |
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
_temp/
|
||||
front/styles/bg_pattern_peace_old.png
|
||||
test.png
|
||||
TODO.md
|
||||
front/images/*
|
||||
front/images/counter/*
|
||||
config.json
|
10
README.md
10
README.md
@@ -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
19
TODO.md
@@ -5,7 +5,7 @@
|
||||
- Детальная стата по инстансу
|
||||
- Демонстрация наполнения и управление БД
|
||||
- "Большая Красная Кнопка"
|
||||
- Общая статистика по инстансу
|
||||
- ~~Общая статистика по инстансу~~
|
||||
- Главная страница
|
||||
- Страница регистрации
|
||||
- Страница с отображением поста-картинки
|
||||
@@ -20,9 +20,8 @@
|
||||
- Айди приглашения
|
||||
- Роли
|
||||
- Новичок
|
||||
- Может оценивать посты, но не более n в день
|
||||
- Может создавать посты, но не более n в день и только с одобренными тегами
|
||||
- Может удалять свои посты
|
||||
- Может оценивать посты
|
||||
- Не может создавать посты
|
||||
- Может устанавливать себе аватарку
|
||||
- Проверенный
|
||||
- Нет лимитов на оценку постов
|
||||
@@ -39,6 +38,7 @@
|
||||
- Может банить кого угодно
|
||||
- Может удалять чьи угодно аккаунты
|
||||
- Может редактировать чужие посты
|
||||
- Может управлять одобренными тегами
|
||||
- Аватарки
|
||||
- Бан
|
||||
- Полное удаление
|
||||
@@ -48,10 +48,10 @@
|
||||
- Приглашения
|
||||
- Регистрация по приглашению автоматически даёт роль "проверенный"
|
||||
- Пост с картинкой
|
||||
- Рекодирование пикчи в низкое разрешение для превью
|
||||
- Описание
|
||||
- Теги
|
||||
- Добавление нового
|
||||
- ~~Рекодирование пикчи в низкое разрешение для превью~~
|
||||
- ~~Описание~~
|
||||
- ~~Теги~~
|
||||
- ~~Добавление нового~~
|
||||
- Редактирование тегов существующего
|
||||
- Удаление
|
||||
- Оценки
|
||||
@@ -59,7 +59,8 @@
|
||||
- Статистика по всем картинкам
|
||||
- Комментарии
|
||||
- Теги
|
||||
- Перечень одобренных
|
||||
- ~~Перечень одобренных~~
|
||||
- Добавление, редактирование и удаление одобренных
|
||||
- Шаблонная разметка
|
||||
- Локализация
|
||||
- Кастомизация внешнего вида
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
?>
|
@@ -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"]);
|
||||
}
|
||||
|
||||
?>
|
||||
|
11
api/_db.php
11
api/_db.php
@@ -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");
|
||||
|
||||
|
||||
|
||||
|
@@ -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
111
api/_errorslist.php
Normal 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
27
api/_input_checks.php
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
?>
|
@@ -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
99
api/_types.php
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@@ -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
154
api/comments/index.php
Normal 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
349
api/post/create.php
Normal 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 .= "<";
|
||||
break;
|
||||
case ">":
|
||||
$title .= ">";
|
||||
break;
|
||||
case "/":
|
||||
$title .= "/";
|
||||
break;
|
||||
case "\\":
|
||||
$title .= "\";
|
||||
break;
|
||||
case "?":
|
||||
$title .= "?";
|
||||
break;
|
||||
case "&":
|
||||
$title .= "&";
|
||||
break;
|
||||
case "\n":
|
||||
$title .= "<br>";
|
||||
break;
|
||||
case "\t":
|
||||
$title .= " ";
|
||||
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
307
api/post/find.php
Normal 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());
|
||||
}
|
||||
|
||||
?>
|
@@ -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
139
api/tags/index.php
Normal 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());
|
||||
}
|
||||
|
||||
?>
|
29
api/user/__admin_session.php
Normal file
29
api/user/__admin_session.php
Normal 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);
|
||||
}
|
||||
?>
|
@@ -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()]);
|
||||
}
|
||||
|
||||
?>
|
@@ -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()]);
|
||||
}
|
||||
}
|
||||
?>
|
@@ -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
89
api/user/login.php
Normal 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()]);
|
||||
}
|
||||
|
||||
?>
|
17
config.json
17
config.json
@@ -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
51
config.json.example
Normal 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
|
||||
}
|
@@ -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
|
||||
|
21
docs/DB.md
21
docs/DB.md
@@ -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!!!
|
@@ -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
|
||||
|
@@ -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
3
front/favicon.html
Normal 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
16
front/footer.php
Normal 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
25
front/head.php
Normal 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
39
front/notifications.php
Normal 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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
?>
|
34
front/pages/index/counter.php
Normal file
34
front/pages/index/counter.php
Normal 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>
|
5
front/pages/index/page.php
Normal file
5
front/pages/index/page.php
Normal 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");
|
||||
?>
|
6
front/pages/index/random_meme.php
Normal file
6
front/pages/index/random_meme.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
// TODO: picking random meme
|
||||
?>
|
||||
<div class="visualbox">
|
||||
<img src="test.png">
|
||||
</div>
|
57
front/pages/index/searchbox.php
Normal file
57
front/pages/index/searchbox.php
Normal 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>
|
55
front/pages/login/page.php
Normal file
55
front/pages/login/page.php
Normal 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't have an account? <a href="./?do=register">Register here</a></p>
|
||||
</div>
|
||||
</div>
|
62
front/pages/main_nav.php
Normal file
62
front/pages/main_nav.php
Normal 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&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>
|
62
front/pages/new_post/page.php
Normal file
62
front/pages/new_post/page.php
Normal 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>
|
99
front/pages/register/page.php
Normal file
99
front/pages/register/page.php
Normal 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>
|
58
front/pages/search_posts/gen_post_entry.php
Normal file
58
front/pages/search_posts/gen_post_entry.php
Normal 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;
|
||||
}
|
||||
|
||||
?>
|
179
front/pages/search_posts/page.php
Normal file
179
front/pages/search_posts/page.php
Normal 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) . "\"><</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) . "\">></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>
|
50
front/pages/stats/page.php
Normal file
50
front/pages/stats/page.php
Normal 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>
|
62
front/pages/tags_viewer/page.php
Normal file
62
front/pages/tags_viewer/page.php
Normal 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>
|
39
front/pages/user_info/page.php
Normal file
39
front/pages/user_info/page.php
Normal 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>
|
106
front/pages/view_post/page.php
Normal file
106
front/pages/view_post/page.php
Normal 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
184
front/styles/base.css
Normal 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 |
@@ -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
11
front/styles/footer.css
Normal 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
65
front/styles/index.css
Normal 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
325
front/styles/main.css
Normal 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;
|
||||
}
|
60
index.html
60
index.html
@@ -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
99
index.php
Normal 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>
|
Reference in New Issue
Block a user