Compare commits
18 commits
f4cb133e46
...
f93696cb14
Author | SHA1 | Date | |
---|---|---|---|
|
f93696cb14 | ||
|
8aa22d899d | ||
|
362b46d0ca | ||
|
5c086b7e93 | ||
|
bc4b47e98a | ||
|
ed19b5dd7a | ||
|
c2f82dd7d0 | ||
|
4e8ed589d3 | ||
|
9a88345057 | ||
|
39193c88c0 | ||
|
950d856489 | ||
|
2009416c35 | ||
|
9e4eb3351e | ||
|
1a266eb6aa | ||
|
08ae408bdf | ||
|
d701885d2b | ||
|
a0671bfba8 | ||
|
5645c5369d |
3
.gitignore
vendored
|
@ -14,4 +14,5 @@ rules.txt
|
|||
/uploads/*
|
||||
t.php
|
||||
logs
|
||||
.txt
|
||||
.txt
|
||||
/storage/*
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use \App\Services\{Router, Auth, DB, Json};
|
||||
|
@ -32,23 +33,20 @@ class AdminController
|
|||
}
|
||||
|
||||
public static function index()
|
||||
|
||||
{
|
||||
|
||||
Page::set('Admin/Index');
|
||||
}
|
||||
public static function loadContent() {
|
||||
public static function loadContent()
|
||||
{
|
||||
$fileName = $_GET['type'];
|
||||
|
||||
|
||||
$filePath = $_SERVER['DOCUMENT_ROOT'].'/views/pages/Admin/' . $fileName.'.php';
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
Page::set('Admin/' . self::$file);
|
||||
} else {
|
||||
Page::set('Admin/General');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$filePath = $_SERVER['DOCUMENT_ROOT'] . '/views/pages/Admin/' . $fileName . '.php';
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
Page::set('Admin/' . self::$file);
|
||||
} else {
|
||||
Page::set('Admin/General');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,16 @@ class SetVisibility
|
|||
if (!array_key_exists('declineReason', $data)) {
|
||||
$data['declineReason'] = null;
|
||||
}
|
||||
if (!array_key_exists('iRate', $data)) {
|
||||
$data['iRate'] = $_GET['irate'];
|
||||
}
|
||||
if (!array_key_exists('kRate', $data)) {
|
||||
$data['kRate'] = $_GET['krate'];
|
||||
}
|
||||
if ($_POST['comment'] != null) {
|
||||
$data['declineComment'] = $_POST['comment'];
|
||||
}
|
||||
|
||||
if ($_GET['mod'] != 1) {
|
||||
$data['declineReason'] = $_GET['reason'];
|
||||
} else {
|
||||
|
|
35
app/Controllers/Api/Emoji/Load.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers\Api\Emoji;
|
||||
|
||||
use \App\Services\Emoji;
|
||||
|
||||
class Load
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
|
||||
|
||||
try {
|
||||
$smileys = Emoji::getAllSmileys();
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => array_map(function ($s) {
|
||||
return [
|
||||
'code' => preg_quote($s['code'], '/'),
|
||||
'url' => $s['url'],
|
||||
'keywords' => explode('_', str_replace('/', '_', $s['code']))
|
||||
];
|
||||
}, $smileys)
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Smileys load failed'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ class Create
|
|||
}
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
$id = $_POST['id'];
|
||||
$postbody = $_POST['wtext'];
|
||||
if ((int)$id === DB::query('SELECT id FROM photos WHERE id=:id', array(':id' => $id))[0]['id']) {
|
||||
|
@ -69,7 +70,21 @@ class Create
|
|||
|
||||
if ((strlen($postbody) < 4096 || strlen($postbody) > 1) || $_FILES['filebody']['error'] != 4) {
|
||||
if (trim($postbody) != '' || $_FILES['filebody']['error'] != 4) {
|
||||
$smileys_dir = $_SERVER['DOCUMENT_ROOT'].'/static/img/smileys/1';
|
||||
|
||||
$allowedCodes = [];
|
||||
$files = scandir($smileys_dir);
|
||||
foreach ($files as $file) {
|
||||
$ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||
if (in_array(strtolower($ext), ['gif', 'png', 'jpg'])) {
|
||||
$allowedCodes[] = ':'.pathinfo($file, PATHINFO_FILENAME).':';
|
||||
}
|
||||
}
|
||||
$postbody = ltrim($postbody);
|
||||
$postbody = preg_replace_callback('/:\w+:/', function($matches) use ($allowedCodes) {
|
||||
return in_array($matches[0], $allowedCodes) ? $matches[0] : '';
|
||||
}, $postbody);
|
||||
|
||||
echo json_encode(
|
||||
array(
|
||||
'errorcode' => '0',
|
||||
|
|
|
@ -2,110 +2,426 @@
|
|||
|
||||
namespace App\Controllers\Api\Images;
|
||||
|
||||
class Compress {
|
||||
private static function compressAndResizeImage($source_url, $quality, $max_width, $max_height) {
|
||||
$info = getimagesize($source_url);
|
||||
class Compress
|
||||
{
|
||||
private const MAX_REDIRECTS = 3;
|
||||
private const CACHE_DIR = '/cdn/imgcache/';
|
||||
private const MAX_CACHE_AGE = 2592000;
|
||||
private const DEFAULT_QUALITY = 20;
|
||||
private const ALLOWED_DOMAINS = NGALLERY['root']['alloweddomains'];
|
||||
private const CSP_HEADER = "default-src 'none'; img-src 'self' data:;";
|
||||
|
||||
if ($info === false) {
|
||||
return false;
|
||||
private $sourceUrl;
|
||||
private $quality;
|
||||
private $width;
|
||||
private $height;
|
||||
private $cachePath;
|
||||
private $config = [
|
||||
'faceDetection' => false,
|
||||
'stripMeta' => true,
|
||||
'bulkMode' => false,
|
||||
'webhook' => null,
|
||||
'resizePercentage' => 35,
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
header("Content-Security-Policy: " . self::CSP_HEADER);
|
||||
try {
|
||||
$this->validateRequest();
|
||||
$this->processRequest();
|
||||
} catch (\Exception $e) {
|
||||
$this->handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateRequest(): void
|
||||
{
|
||||
$params = $_GET;
|
||||
unset($params['sig']);
|
||||
|
||||
ksort($params);
|
||||
|
||||
$this->sourceUrl = $_GET['url'] ?? '';
|
||||
$this->quality = $this->getQualityParam();
|
||||
$this->width = (int)($_GET['width'] ?? 0);
|
||||
$this->height = (int)($_GET['height'] ?? 0);
|
||||
|
||||
$parsed = parse_url($this->sourceUrl);
|
||||
$docRoot = realpath($_SERVER['DOCUMENT_ROOT']);
|
||||
|
||||
if (!isset($parsed['scheme'])) {
|
||||
$sourcePath = ltrim($parsed['path'] ?? '', '/');
|
||||
$localFullPath = realpath($docRoot . '/' . $sourcePath);
|
||||
|
||||
if (!$localFullPath || !is_file($localFullPath)) {
|
||||
throw new \RuntimeException('Local file not found', 404);
|
||||
}
|
||||
|
||||
if (strpos($localFullPath, $docRoot) !== 0) {
|
||||
throw new \RuntimeException('Access denied', 403);
|
||||
}
|
||||
|
||||
$this->sourceUrl = $localFullPath;
|
||||
} elseif (!in_array($parsed['host'], self::ALLOWED_DOMAINS)) {
|
||||
throw new \DomainException('Domain not allowed', 403);
|
||||
}
|
||||
}
|
||||
|
||||
private function getQualityParam(): int
|
||||
{
|
||||
$quality = (int)($_GET['quality'] ?? self::DEFAULT_QUALITY);
|
||||
|
||||
if (isset($_SERVER['HTTP_SAVE_DATA']) && $_SERVER['HTTP_SAVE_DATA'] === 'on') {
|
||||
$quality = max(30, $quality - 20);
|
||||
}
|
||||
|
||||
$width = $info[0];
|
||||
$height = $info[1];
|
||||
$aspect_ratio = $width / $height;
|
||||
return min(95, max(10, $quality));
|
||||
}
|
||||
|
||||
if ($width > $height) {
|
||||
$new_width = $max_width;
|
||||
$new_height = $max_width / $aspect_ratio;
|
||||
private function processRequest(): void
|
||||
{
|
||||
if ($this->config['bulkMode']) {
|
||||
$this->processBulk();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->generateCachePath();
|
||||
|
||||
if ($this->serveFromCache()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$imageData = $this->fetchImage();
|
||||
$processed = $this->processImage($imageData);
|
||||
|
||||
$this->saveToCache($processed);
|
||||
$this->sendResponse($processed);
|
||||
|
||||
if ($this->config['webhook']) {
|
||||
$this->callWebhook(strlen($imageData), strlen($processed));
|
||||
}
|
||||
}
|
||||
|
||||
private function generateCachePath(): void
|
||||
{
|
||||
$params = [
|
||||
'url' => $this->sourceUrl,
|
||||
'q' => $this->quality,
|
||||
'w' => $this->width,
|
||||
'h' => $this->height,
|
||||
'strip' => $this->config['stripMeta'],
|
||||
'resizePct' => $this->config['resizePercentage'],
|
||||
];
|
||||
|
||||
$hash = md5(serialize($params));
|
||||
$subdir = substr($hash, 0, 2);
|
||||
$this->cachePath = $_SERVER['DOCUMENT_ROOT'] . self::CACHE_DIR . $subdir . '/' . $hash . '.jpg';
|
||||
}
|
||||
|
||||
private function serveFromCache(): bool
|
||||
{
|
||||
if (file_exists($this->cachePath)) {
|
||||
$lastModified = filemtime($this->cachePath);
|
||||
|
||||
if (time() - $lastModified < self::MAX_CACHE_AGE) {
|
||||
header('Content-Type: image/jpeg');
|
||||
header('Content-Length: ' . filesize($this->cachePath));
|
||||
header('Cache-Control: max-age=' . self::MAX_CACHE_AGE);
|
||||
readfile($this->cachePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
unlink($this->cachePath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function saveToCache(string $data): void
|
||||
{
|
||||
$dir = dirname($this->cachePath);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
$tempFile = tempnam($dir, 'tmp_');
|
||||
if (file_put_contents($tempFile, $data)) {
|
||||
rename($tempFile, $this->cachePath);
|
||||
} else {
|
||||
$new_height = $max_height;
|
||||
$new_width = $max_height * $aspect_ratio;
|
||||
unlink($tempFile);
|
||||
throw new \RuntimeException('Failed to save cache');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function fetchImage(): string
|
||||
{
|
||||
// Для локальных файлов
|
||||
if ($this->isLocalFile()) {
|
||||
$data = file_get_contents($this->sourceUrl);
|
||||
|
||||
if ($data === false) {
|
||||
throw new \RuntimeException('Failed to read local file', 500);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
if ($info['mime'] == 'image/jpeg') {
|
||||
$image = imagecreatefromjpeg($source_url);
|
||||
} elseif ($info['mime'] == 'image/gif') {
|
||||
$image = imagecreatefromgif($source_url);
|
||||
} elseif ($info['mime'] == 'image/png') {
|
||||
$image = imagecreatefrompng($source_url);
|
||||
} else {
|
||||
return false;
|
||||
// Для удаленных URL
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $this->sourceUrl,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => self::MAX_REDIRECTS,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => true
|
||||
]);
|
||||
|
||||
$data = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
throw new \RuntimeException('Fetch failed: ' . curl_error($ch), 500);
|
||||
}
|
||||
|
||||
$resized_image = imagecreatetruecolor($new_width, $new_height);
|
||||
|
||||
if ($info['mime'] == 'image/png' || $info['mime'] == 'image/gif') {
|
||||
imagealphablending($resized_image, false);
|
||||
imagesavealpha($resized_image, true);
|
||||
$transparent = imagecolorallocatealpha($resized_image, 255, 255, 255, 127);
|
||||
imagefilledrectangle($resized_image, 0, 0, $new_width, $new_height, $transparent);
|
||||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($status !== 200) {
|
||||
throw new \RuntimeException("HTTP error $status", $status);
|
||||
}
|
||||
|
||||
imagecopyresampled($resized_image, $image, 0, 0, 0, 0, $new_width, $new_height, $width, $height);
|
||||
curl_close($ch);
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function isLocalFile(): bool
|
||||
{
|
||||
return is_string($this->sourceUrl)
|
||||
&& strpos($this->sourceUrl, '://') === false
|
||||
&& file_exists($this->sourceUrl);
|
||||
}
|
||||
|
||||
private function processImage(string $imageData): string
|
||||
{
|
||||
$isJpeg = $this->isJpeg($imageData);
|
||||
$noChanges = $this->quality === 100
|
||||
&& $this->width === 0
|
||||
&& $this->height === 0
|
||||
&& !$this->config['stripMeta'];
|
||||
$isLocal = $this->isLocalFile();
|
||||
|
||||
if ($isJpeg && $noChanges && !$isLocal) {
|
||||
return $imageData;
|
||||
}
|
||||
|
||||
$image = @imagecreatefromstring($imageData);
|
||||
if ($image === false) {
|
||||
throw new \RuntimeException('Unsupported image format', 400);
|
||||
}
|
||||
|
||||
if ($isJpeg) {
|
||||
$image = $this->fixImageOrientation($image, $imageData);
|
||||
}
|
||||
|
||||
// Расчет размеров с учетом процента
|
||||
$targetWidth = $this->width;
|
||||
$targetHeight = $this->height;
|
||||
|
||||
if ($this->config['resizePercentage'] && $targetWidth === 0 && $targetHeight === 0) {
|
||||
$origWidth = imagesx($image);
|
||||
$origHeight = imagesy($image);
|
||||
|
||||
$ratio = $this->config['resizePercentage'] / 100;
|
||||
$targetWidth = round($origWidth * $ratio);
|
||||
$targetHeight = round($origHeight * $ratio);
|
||||
}
|
||||
|
||||
if ($targetWidth > 0 || $targetHeight > 0) {
|
||||
$image = $this->resizeImage($image, $targetWidth, $targetHeight);
|
||||
}
|
||||
|
||||
if ($this->config['stripMeta']) {
|
||||
$this->stripMetadata($image);
|
||||
}
|
||||
|
||||
if (!imageistruecolor($image)) {
|
||||
$tmp = imagecreatetruecolor(imagesx($image), imagesy($image));
|
||||
imagecopy($tmp, $image, 0, 0, 0, 0, imagesx($image), imagesy($image));
|
||||
imagedestroy($image);
|
||||
$image = $tmp;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
imagejpeg($resized_image, null, $quality);
|
||||
$compressed_image_data = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
imageinterlace($image, true);
|
||||
imagejpeg($image, null, $this->quality);
|
||||
$result = ob_get_clean();
|
||||
imagedestroy($image);
|
||||
imagedestroy($resized_image);
|
||||
|
||||
return $compressed_image_data;
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function generateCacheFilename($source_url, $quality, $max_width, $max_height) {
|
||||
return $_SERVER['DOCUMENT_ROOT'].'/cdn/imgcache/' . md5($source_url . $quality . $max_width . $max_height) . '.jpg';
|
||||
private function isJpeg(string $data): bool
|
||||
{
|
||||
return bin2hex(substr($data, 0, 2)) === 'ffd8';
|
||||
}
|
||||
|
||||
public function __construct() {
|
||||
$source_url = $_GET['url'];
|
||||
$quality = 40;
|
||||
$max_width = 400;
|
||||
$max_height = 400;
|
||||
|
||||
if (!file_exists($_SERVER['DOCUMENT_ROOT'].'/cdn/imgcache')) {
|
||||
mkdir($_SERVER['DOCUMENT_ROOT'].'/cdn/imgcache', 0777, true);
|
||||
private function fixImageOrientation($image, string $imageData)
|
||||
{
|
||||
try {
|
||||
$exif = @exif_read_data('data://image/jpeg;base64,' . base64_encode($imageData));
|
||||
} catch (\Exception $e) {
|
||||
return $image;
|
||||
}
|
||||
|
||||
$parsed_url = parse_url($source_url);
|
||||
if (!isset($parsed_url['scheme'])) {
|
||||
$local_file_path = $_SERVER['DOCUMENT_ROOT'] . '/' . ltrim($source_url, '/');
|
||||
if (file_exists($local_file_path)) {
|
||||
$source_url = $local_file_path;
|
||||
} else {
|
||||
header("HTTP/1.0 404 Not Found");
|
||||
exit;
|
||||
if (!empty($exif['Orientation'])) {
|
||||
switch ($exif['Orientation']) {
|
||||
case 3:
|
||||
$image = imagerotate($image, 180, 0);
|
||||
break;
|
||||
case 6:
|
||||
$image = imagerotate($image, -90, 0);
|
||||
break;
|
||||
case 8:
|
||||
$image = imagerotate($image, 90, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$cache_filename = self::generateCacheFilename($source_url, $quality, $max_width, $max_height);
|
||||
return $image;
|
||||
}
|
||||
|
||||
if (file_exists($cache_filename)) {
|
||||
$compressed_image_data = file_get_contents($cache_filename);
|
||||
private function resizeImage($image, int $targetWidth, int $targetHeight)
|
||||
{
|
||||
$origWidth = imagesx($image);
|
||||
$origHeight = imagesy($image);
|
||||
|
||||
if ($targetWidth > 0 && $targetHeight === 0) {
|
||||
$targetHeight = round($origHeight * ($targetWidth / $origWidth));
|
||||
} elseif ($targetHeight > 0 && $targetWidth === 0) {
|
||||
$targetWidth = round($origWidth * ($targetHeight / $origHeight));
|
||||
}
|
||||
|
||||
$resized = imagecreatetruecolor($targetWidth, $targetHeight);
|
||||
imagealphablending($resized, false);
|
||||
imagesavealpha($resized, true);
|
||||
imagecopyresampled(
|
||||
$resized,
|
||||
$image,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$targetWidth,
|
||||
$targetHeight,
|
||||
$origWidth,
|
||||
$origHeight
|
||||
);
|
||||
imagedestroy($image);
|
||||
|
||||
return $resized;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function stripMetadata(&$image): void
|
||||
{
|
||||
$width = imagesx($image);
|
||||
$height = imagesy($image);
|
||||
$clean = imagecreatetruecolor($width, $height);
|
||||
|
||||
imagealphablending($clean, false);
|
||||
imagesavealpha($clean, true);
|
||||
$transparent = imagecolorallocatealpha($clean, 0, 0, 0, 127);
|
||||
imagefill($clean, 0, 0, $transparent);
|
||||
|
||||
imagecopy($clean, $image, 0, 0, 0, 0, $width, $height);
|
||||
imagedestroy($image);
|
||||
$image = $clean;
|
||||
}
|
||||
|
||||
private function processBulk(): void
|
||||
{
|
||||
$jobs = json_decode(file_get_contents('php://input'), true);
|
||||
$results = [];
|
||||
|
||||
foreach ($jobs as $job) {
|
||||
try {
|
||||
$this->sourceUrl = $job['url'];
|
||||
$this->quality = $job['quality'] ?? $this->quality;
|
||||
|
||||
$imageData = $this->fetchImage();
|
||||
$processed = $this->processImage($imageData);
|
||||
|
||||
$results[] = [
|
||||
'url' => $job['url'],
|
||||
'status' => 'success',
|
||||
'size' => strlen($processed),
|
||||
'data' => base64_encode($processed)
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$results[] = [
|
||||
'url' => $job['url'],
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($results);
|
||||
exit;
|
||||
}
|
||||
|
||||
private function sendResponse(string $imageData): void
|
||||
{
|
||||
header('Content-Type: image/jpeg');
|
||||
header('Content-Length: ' . strlen($imageData));
|
||||
header('Cache-Control: max-age=' . self::MAX_CACHE_AGE);
|
||||
echo $imageData;
|
||||
}
|
||||
|
||||
private function callWebhook(int $origSize, int $processedSize): void
|
||||
{
|
||||
$payload = [
|
||||
'url' => $this->sourceUrl,
|
||||
'originalSize' => $origSize,
|
||||
'processedSize' => $processedSize,
|
||||
'timestamp' => time()
|
||||
];
|
||||
|
||||
$ch = curl_init($this->config['webhook']);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 2,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_POSTFIELDS => json_encode($payload)
|
||||
]);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
private function handleError(\Exception $e): void
|
||||
{
|
||||
$code = $e->getCode() >= 400 ? $e->getCode() : 500;
|
||||
http_response_code($code);
|
||||
|
||||
if ($this->config['bulkMode']) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'error' => $e->getMessage(),
|
||||
'code' => $code
|
||||
]);
|
||||
} else {
|
||||
$compressed_image_data = self::compressAndResizeImage($source_url, $quality, $max_width, $max_height);
|
||||
|
||||
if ($compressed_image_data) {
|
||||
file_put_contents($cache_filename, $compressed_image_data);
|
||||
$brokenImgPath = $_SERVER['DOCUMENT_ROOT'] . '/static/img/brokenimg.png';
|
||||
if (file_exists($brokenImgPath) && is_file($brokenImgPath)) {
|
||||
header('Content-Type: image/png');
|
||||
header('Cache-Control: no-store');
|
||||
readfile($brokenImgPath);
|
||||
} else {
|
||||
$imageData = file_get_contents($source_url);
|
||||
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->buffer($imageData);
|
||||
|
||||
header("Content-Type: $mimeType");
|
||||
|
||||
echo $imageData;
|
||||
exit;
|
||||
header('Content-Type: text/plain');
|
||||
echo "Error $code: " . $e->getMessage() . " (Fallback image not found)";
|
||||
}
|
||||
}
|
||||
|
||||
if ($compressed_image_data) {
|
||||
header('Content-Type: image/jpeg');
|
||||
header('Content-Length: ' . strlen($compressed_image_data));
|
||||
echo $compressed_image_data;
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
110
app/Controllers/Api/Images/LoadMap.php
Normal file
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers\Api\Images;
|
||||
|
||||
use \App\Services\{DB, Image};
|
||||
use Symfony\Component\Process\Process;
|
||||
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||
|
||||
class LoadMap
|
||||
{
|
||||
private const CHUNK_SIZE = 25;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
error_log("API Request: " . json_encode($_GET));
|
||||
|
||||
$bounds = $this->validateBounds($_GET);
|
||||
$photos = $this->fetchPhotos($bounds);
|
||||
|
||||
error_log("Fetched photos count: " . count($photos));
|
||||
|
||||
$validPhotos = $this->parallelProcessing($photos);
|
||||
|
||||
echo json_encode($validPhotos);
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateBounds(array $get): array
|
||||
{
|
||||
return [
|
||||
'north' => (float)($get['north'] ?? 90),
|
||||
'south' => (float)($get['south'] ?? -90),
|
||||
'west' => (float)($get['west'] ?? -180),
|
||||
'east' => (float)($get['east'] ?? 180)
|
||||
];
|
||||
}
|
||||
|
||||
private function fetchPhotos(array $bounds): array
|
||||
{
|
||||
return DB::query("
|
||||
SELECT p.id, p.photourl, p.content
|
||||
FROM photos p
|
||||
WHERE
|
||||
JSON_VALUE(p.content, '$.lat') BETWEEN ? AND ? AND
|
||||
JSON_VALUE(p.content, '$.lng') BETWEEN ? AND ?
|
||||
LIMIT 100
|
||||
", [$bounds['south'], $bounds['north'], $bounds['west'], $bounds['east']]);
|
||||
}
|
||||
|
||||
private function parallelProcessing(array $photos): array
|
||||
{
|
||||
$result = [];
|
||||
$scriptPath = str_replace('/', DIRECTORY_SEPARATOR, $_SERVER['DOCUMENT_ROOT'] . '/app/Controllers/Exec/Tasks/BlurNewImage.php');
|
||||
|
||||
$chunks = array_chunk($photos, self::CHUNK_SIZE);
|
||||
$processes = [];
|
||||
|
||||
try {
|
||||
foreach ($chunks as $chunk) {
|
||||
$process = new Process(
|
||||
['php', $scriptPath],
|
||||
null,
|
||||
null,
|
||||
json_encode($chunk)
|
||||
);
|
||||
$process->start();
|
||||
$processes[] = $process;
|
||||
error_log("Started process PID: " . $process->getPid());
|
||||
}
|
||||
|
||||
while (count($processes)) {
|
||||
foreach ($processes as $i => $process) {
|
||||
if ($process->isRunning()) continue;
|
||||
|
||||
if (!$process->isSuccessful()) {
|
||||
error_log("Process failed: " . $process->getErrorOutput());
|
||||
throw new ProcessFailedException($process);
|
||||
}
|
||||
|
||||
$output = json_decode($process->getOutput(), true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \RuntimeException("Invalid JSON response from worker");
|
||||
}
|
||||
|
||||
$result = array_merge($result, $output);
|
||||
unset($processes[$i]);
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
foreach ($processes as $process) {
|
||||
if ($process->isRunning()) {
|
||||
$process->stop(0);
|
||||
}
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
|
@ -2,72 +2,123 @@
|
|||
|
||||
namespace App\Controllers\Api\Images;
|
||||
|
||||
use \App\Services\{Auth, DB, Date, HTMLParser};
|
||||
use \App\Services\{Auth, DB, Date, HTMLParser, Image};
|
||||
use DOMDocument, DOMXPath;
|
||||
|
||||
class LoadRecent
|
||||
{
|
||||
private const CACHE_DIR = __DIR__ . '/../../../../storage/cache/recent/';
|
||||
private const CACHE_TTL = 300;
|
||||
private const BATCH_SIZE = 30;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$response = [];
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$this->ensureCacheDirExists();
|
||||
|
||||
echo $this->handleLocalRequest();
|
||||
} catch (\Exception $e) {
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($_POST['serverhost'] != 'transphoto.org') {
|
||||
$photos = DB::query('SELECT * FROM photos WHERE moderated=1 AND id<:id ORDER BY id DESC LIMIT 30', array(':id'=>$_GET['lastpid']));
|
||||
private function ensureCacheDirExists(): void
|
||||
{
|
||||
if (!file_exists(self::CACHE_DIR)) {
|
||||
mkdir(self::CACHE_DIR, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleLocalRequest(): string
|
||||
{
|
||||
$cacheKey = 'recent_' . md5(serialize($_GET));
|
||||
$cacheFile = self::CACHE_DIR . $cacheKey;
|
||||
|
||||
foreach ($photos as $p) {
|
||||
if ($p['posted_at'] === 943909200 || Date::zmdate($p['posted_at']) === '30 ноября 1999 в 00:00') {
|
||||
$date = 'дата не указана';
|
||||
} else {
|
||||
$date = Date::zmdate($p['posted_at']);
|
||||
}
|
||||
$user = DB::query('SELECT * FROM users WHERE id=:id', array(':id' => $p['user_id']))[0];
|
||||
$comments = DB::query('SELECT COUNT(*) FROM photos_comments WHERE photo_id=:pid', array(':pid'=>$p['id']))[0]['COUNT(*)'];
|
||||
$response[] = [
|
||||
'id' => $p['id'],
|
||||
'place' => htmlspecialchars($p['place']),
|
||||
'date' => $date,
|
||||
'user_name' => $user['username'],
|
||||
'user_id' => $p['user_id'],
|
||||
'photourl' => $p['photourl'],
|
||||
'photourl_small' => 'https://' . $_SERVER['SERVER_NAME'] . '/api/photo/compress?url=' . $p['photourl'],
|
||||
'ccnt' => $comments
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$url = 'https://transphoto.org/api.php?action=get-recent-photos&width=802&lastpid=0&hidden=0';
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$responsed = curl_exec($ch);
|
||||
if (curl_errno($ch)) {
|
||||
$response = [
|
||||
'error' => 1,
|
||||
'errorcode' => 'СТТС не отвечает. Попробуйте позже',
|
||||
];
|
||||
} else {
|
||||
$data = json_decode($responsed, true);
|
||||
foreach ($data as $d) {
|
||||
$response[] = [
|
||||
'id' => $d['pid'],
|
||||
'place' => strip_tags($d['links']),
|
||||
'date' => $d['pdate'],
|
||||
'photourl_small' => 'https://transphoto.org'.$d['prw'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
|
||||
|
||||
if (file_exists($cacheFile) && time() - filemtime($cacheFile) < self::CACHE_TTL) {
|
||||
return file_get_contents($cacheFile);
|
||||
}
|
||||
|
||||
$photos = $this->fetchPhotos();
|
||||
$userIds = array_column($photos, 'user_id');
|
||||
$users = $this->fetchUsers($userIds);
|
||||
$commentsCount = $this->fetchCommentsCount(array_column($photos, 'id'));
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
$response = [];
|
||||
foreach ($photos as $p) {
|
||||
$response[] = $this->formatPhotoData($p, $users[$p['user_id']] ?? [], $commentsCount[$p['id']] ?? 0);
|
||||
}
|
||||
|
||||
$jsonResponse = json_encode($response);
|
||||
file_put_contents($cacheFile, $jsonResponse);
|
||||
|
||||
return $jsonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchPhotos(): array
|
||||
{
|
||||
return DB::query(
|
||||
'SELECT * FROM photos
|
||||
WHERE moderated = 1 AND id < :id
|
||||
ORDER BY id DESC
|
||||
LIMIT ' . self::BATCH_SIZE,
|
||||
[':id' => $_GET['lastpid'] ?? 0]
|
||||
);
|
||||
}
|
||||
|
||||
private function fetchUsers(array $userIds): array
|
||||
{
|
||||
if (empty($userIds)) return [];
|
||||
|
||||
$users = DB::query(
|
||||
'SELECT id, username FROM users
|
||||
WHERE id IN (' . implode(',', array_map('intval', $userIds)) . ')'
|
||||
);
|
||||
|
||||
return array_combine(array_column($users, 'id'), $users);
|
||||
}
|
||||
|
||||
private function fetchCommentsCount(array $photoIds): array
|
||||
{
|
||||
if (empty($photoIds)) return [];
|
||||
|
||||
$counts = DB::query(
|
||||
'SELECT photo_id, COUNT(*) as cnt
|
||||
FROM photos_comments
|
||||
WHERE photo_id IN (' . implode(',', array_map('intval', $photoIds)) . ')
|
||||
GROUP BY photo_id'
|
||||
);
|
||||
|
||||
return array_combine(array_column($counts, 'photo_id'), array_column($counts, 'cnt'));
|
||||
}
|
||||
|
||||
private function formatPhotoData(array $photo, array $user, int $comments): array
|
||||
{
|
||||
return [
|
||||
'id' => $photo['id'],
|
||||
'place' => htmlspecialchars($photo['place']),
|
||||
'date' => $this->formatDate($photo['posted_at']),
|
||||
'user_name' => $user['username'] ?? 'Unknown',
|
||||
'user_id' => $photo['user_id'],
|
||||
'photourl' => $photo['photourl'],
|
||||
'photourl_small' => $this->generateSmallUrl($photo['photourl']),
|
||||
'photourl_extrasmall' => Image::generateBlurredPlaceholder($photo['photourl']),
|
||||
'ccnt' => $comments
|
||||
];
|
||||
}
|
||||
|
||||
private function formatDate(int $timestamp): string
|
||||
{
|
||||
if ($timestamp === 943909200 || Date::zmdate($timestamp) === '30 ноября 1999 в 00:00') {
|
||||
return 'дата не указана';
|
||||
}
|
||||
return Date::zmdate($timestamp);
|
||||
}
|
||||
|
||||
private function generateSmallUrl(string $url): string
|
||||
{
|
||||
return 'https://' . $_SERVER['SERVER_NAME'] . '/api/photo/compress?url=' . urlencode($url);
|
||||
}
|
||||
|
||||
}
|
|
@ -102,11 +102,9 @@ class Upload
|
|||
imagejpeg($background, $outputImagePath, 90);
|
||||
imagedestroy($background);
|
||||
imagedestroy($overlay);
|
||||
|
||||
$upload = new UploadPhoto($outputImagePath, 'cdn/img/');
|
||||
self::$vidpreview = $upload->getSrc();
|
||||
$upload = new UploadPhoto($mp4File, 'cdn/video/');
|
||||
echo explode($mp4File, '.')[1];
|
||||
self::$videourl = $upload->getSrc();
|
||||
$exif = Json::return(
|
||||
array(
|
||||
|
|
|
@ -43,9 +43,22 @@ class Login
|
|||
|
||||
$data = json_decode($response, true);
|
||||
$loc = $data['country'] . ', ' . $data['city'];
|
||||
DB::query('INSERT INTO login_tokens VALUES (\'0\', :token, :user_id)', array(
|
||||
$device = $ua->platform();
|
||||
$os = $ua->platform();
|
||||
$encryptionKey = NGALLERY['root']['encryptionkey'];
|
||||
|
||||
$iv = openssl_random_pseudo_bytes(16);
|
||||
$encryptedIp = openssl_encrypt($ip, 'AES-256-CBC', $encryptionKey, 0, $iv);
|
||||
$encryptedLoc = openssl_encrypt($loc, 'AES-256-CBC', $encryptionKey, 0, $iv);
|
||||
DB::query('INSERT INTO login_tokens VALUES (\'0\', :token, :user_id, :device, :os, :ip, :loc, :la, :crd)', array(
|
||||
':token' => $token,
|
||||
':user_id' => $user_id,
|
||||
':device' => $device,
|
||||
':os' => $os,
|
||||
':ip' => $encryptedIp,
|
||||
':loc' => $encryptedLoc,
|
||||
':la' => time(),
|
||||
':crd' => time()
|
||||
));
|
||||
|
||||
setcookie("NGALLERYSESS", $token, time() + 50 * 50 * 54 * 72, '/', NULL, NULL, TRUE);
|
||||
|
|
|
@ -299,7 +299,7 @@ class Register
|
|||
$status = 0;
|
||||
if (!self::checkforb($_POST['username'], $forbusernames)) {
|
||||
|
||||
if (!strcasecmp(DB::query('SELECT username FROM users WHERE (LOWER(username) LIKE :username)', array(':username' => '%' . $username . '%'))[0]['username'], $username) === false) {
|
||||
if (!strcasecmp(DB::query('SELECT username FROM users WHERE (LOWER(username) LIKE :username)', array(':username' => '%' . $username . '%'))[0]['username'], $username) === false && !in_array(strtolower($username), array_map('strtolower', Router::getRouteSegments()))) {
|
||||
if (Word::strlen(ltrim($username)) >= 2 && Word::strlen(ltrim($username)) <= 20) {
|
||||
|
||||
|
||||
|
@ -427,15 +427,35 @@ class Register
|
|||
} else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
$parser = new UserAgentParser();
|
||||
|
||||
$ua = $parser->parse();
|
||||
$ua = $parser();
|
||||
|
||||
$servicekey = GenerateRandomStr::gen_uuid();
|
||||
$url = 'http://ip-api.com/json/' . $ip;
|
||||
|
||||
$response = file_get_contents($url);
|
||||
|
||||
$data = json_decode($response, true);
|
||||
DB::query('INSERT INTO login_tokens VALUES (\'0\', :token, :user_id)', array(
|
||||
$loc = $data['country'] . ', ' . $data['city'];
|
||||
$device = $ua->platform();
|
||||
$os = $ua->platform();
|
||||
$encryptionKey = NGALLERY['root']['encryptionkey'];
|
||||
|
||||
$iv = openssl_random_pseudo_bytes(16);
|
||||
$encryptedIp = openssl_encrypt($ip, 'AES-256-CBC', $encryptionKey, 0, $iv);
|
||||
$encryptedLoc = openssl_encrypt($loc, 'AES-256-CBC', $encryptionKey, 0, $iv);
|
||||
DB::query('INSERT INTO login_tokens VALUES (\'0\', :token, :user_id, :device, :os, :ip, :loc, :la, :crd, :iv)', array(
|
||||
':token' => $token,
|
||||
':user_id' => $user_id,
|
||||
|
||||
':device' => $device,
|
||||
':os' => $os,
|
||||
':ip' => $encryptedIp,
|
||||
':loc' => $encryptedLoc,
|
||||
':la' => time(),
|
||||
':crd' => time(),
|
||||
':iv' => $iv
|
||||
));
|
||||
|
||||
setcookie("NGALLERYSESS", $token, time() + 120 * 180 * 240 * 720, '/', NULL, NULL, TRUE);
|
||||
|
|
26
app/Controllers/Api/Users/Search.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers\Api\Users;
|
||||
|
||||
|
||||
|
||||
use App\Services\{Auth, Router, GenerateRandomStr, DB, Json, EXIF};
|
||||
use App\Models\{User, Vote};
|
||||
use \App\Core\Page;
|
||||
|
||||
class Search
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$query = $_GET['q'];
|
||||
$users = DB::query('SELECT * FROM users WHERE (LOWER(username) LIKE :username) LIMIT 10', array(':username' => '%' . $query . '%'));
|
||||
foreach ($users as $u) {
|
||||
$result[] = [
|
||||
'id' => $u['id'],
|
||||
'username' => $u['username'],
|
||||
'photourl' => $u['photourl'],
|
||||
];
|
||||
}
|
||||
echo json_encode($result);
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ use \App\Controllers\Api\Images\Rate as PhotoVote;
|
|||
use \App\Controllers\Api\Images\Compress as PhotoCompress;
|
||||
use \App\Controllers\Api\Images\CheckAll as PhotoCheckAll;
|
||||
use \App\Controllers\Api\Images\LoadRecent as PhotoLoadRecent;
|
||||
use \App\Controllers\Api\Images\LoadMap as PhotoLoadMap;
|
||||
use \App\Controllers\Api\Images\Favorite as PhotoFavorite;
|
||||
use \App\Controllers\Api\Images\Stats as PhotoStats;
|
||||
use \App\Controllers\Api\Images\Comments\Create as PhotoComment;
|
||||
|
@ -27,6 +28,7 @@ use \App\Controllers\Api\Vehicles\Load as VehiclesLoad;
|
|||
use \App\Controllers\Api\Profile\Update as ProfileUpdate;
|
||||
use \App\Controllers\Api\Users\LoadUser as UserLoad;
|
||||
use \App\Controllers\Api\Users\EmailVerify as EmailVerify;
|
||||
use \App\Controllers\Api\Users\Search as UsersSearch;
|
||||
use \App\Controllers\Api\Admin\Images\SetVisibility as AdminPhotoSetVisibility;
|
||||
use \App\Controllers\Api\Admin\CreateNews as AdminCreateNews;
|
||||
use \App\Controllers\Api\Admin\LoadNews as AdminLoadNews;
|
||||
|
@ -37,6 +39,11 @@ use \App\Controllers\Api\Admin\GeoDB\Delete as AdminGeoDBDelete;
|
|||
use \App\Controllers\Api\Admin\Contests\CreateTheme as AdminContestsCreateTheme;
|
||||
use \App\Controllers\Api\Admin\Contests\Create as AdminContestsCreate;
|
||||
use \App\Controllers\Api\Admin\Settings\TaskManager as AdminTaskManager;
|
||||
use \App\Controllers\Api\Messages\GetChats as MSGGetChats;
|
||||
use \App\Controllers\Api\Messages\UploadFile as MSGUpload;
|
||||
use \App\Controllers\Api\Messages\GetUsers as MSGGetUsers;
|
||||
use \App\Controllers\Api\Messages\CreateChat as MSGCreateChat;
|
||||
use \App\Controllers\Api\Emoji\Load as EmojiLoad;
|
||||
|
||||
class ApiController
|
||||
{
|
||||
|
@ -144,6 +151,27 @@ class ApiController
|
|||
public static function contestsgetinfo() {
|
||||
return new ContestsGetInfo();
|
||||
}
|
||||
public static function msggetchats() {
|
||||
return new MSGGetChats();
|
||||
}
|
||||
public static function msgupload() {
|
||||
return new MSGUpload();
|
||||
}
|
||||
public static function msggetusers() {
|
||||
return new MSGGetUsers();
|
||||
}
|
||||
public static function msgcreatechat() {
|
||||
return new MSGCreateChat();
|
||||
}
|
||||
public static function userssearch() {
|
||||
return new UsersSearch();
|
||||
}
|
||||
public static function emojiload() {
|
||||
return new EmojiLoad();
|
||||
}
|
||||
public static function photoloadmap() {
|
||||
return new PhotoLoadMap();
|
||||
}
|
||||
|
||||
|
||||
}
|
43
app/Controllers/Exec/Tasks/BlurNewImage.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
require __DIR__.'/../../../../vendor/autoload.php';
|
||||
|
||||
use App\Services\Image;
|
||||
|
||||
try {
|
||||
error_log("BlurNewImage process started");
|
||||
|
||||
$input = json_decode(file_get_contents('php://stdin'), true);
|
||||
|
||||
if (!is_array($input)) {
|
||||
throw new \RuntimeException("Invalid input format");
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($input as $item) {
|
||||
try {
|
||||
if (!isset($item['id'], $item['photourl'], $item['content'])) {
|
||||
throw new \RuntimeException("Invalid item format");
|
||||
}
|
||||
|
||||
$content = json_decode($item['content'], true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$result[] = [
|
||||
'id' => $item['id'],
|
||||
'photourl_small' => Image::generateBlurredPlaceholder($item['photourl']),
|
||||
'photourl' => $item['photourl'],
|
||||
'lat' => (float)$content['lat'],
|
||||
'lng' => (float)$content['lng']
|
||||
];
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Error processing item {$item['id']}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($result);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Critical error in BlurNewImage: " . $e->getMessage());
|
||||
echo json_encode([]);
|
||||
exit(1);
|
||||
}
|
|
@ -1,4 +1,10 @@
|
|||
tasks:
|
||||
- id: "ExecContests"
|
||||
type: "cron"
|
||||
handler: "/app/Controllers/Exec/Tasks/ExecContests.php"
|
||||
handler: "/app/Controllers/Exec/Tasks/ExecContests.php"
|
||||
- id: "BlurNewImage"
|
||||
type: "worker"
|
||||
handler: "/app/Controllers/Exec/Tasks/BlurNewImage.php"
|
||||
- id: "CompressImage"
|
||||
type: "worker"
|
||||
handler: "/app/Controllers/Exec/Tasks/CompressImage.php"
|
|
@ -26,6 +26,11 @@ class MainController
|
|||
{
|
||||
Page::set('About');
|
||||
|
||||
}
|
||||
public static function mapmedia()
|
||||
{
|
||||
Page::set('MapMedia');
|
||||
|
||||
}
|
||||
public static function rules()
|
||||
{
|
||||
|
|
|
@ -8,6 +8,37 @@ use \App\Core\Page;
|
|||
class ProfileController
|
||||
{
|
||||
|
||||
static $file = 'Index';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (isset($_GET['type'])) {
|
||||
switch (Page::exists('Profile/LK/Profile/' . $_GET['type'])) {
|
||||
case true:
|
||||
self::$file = $_GET['type'];
|
||||
break;
|
||||
case false:
|
||||
self::$file = 'Index';
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
self::$file = 'Index';
|
||||
}
|
||||
}
|
||||
|
||||
public static function loadContent()
|
||||
{
|
||||
$fileName = $_GET['type'];
|
||||
|
||||
|
||||
$filePath = $_SERVER['DOCUMENT_ROOT'] . '/views/pages/Profile/LK/Profile/' . $fileName . '.php';
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
Page::set('Profile/LK/Profile/' . self::$file);
|
||||
} else {
|
||||
Page::set('Profile/LK/Profile/Index');
|
||||
}
|
||||
}
|
||||
|
||||
public static function lk()
|
||||
{
|
||||
|
@ -34,6 +65,10 @@ class ProfileController
|
|||
{
|
||||
Page::set('Profile/LK/Profile');
|
||||
}
|
||||
public static function editphoto()
|
||||
{
|
||||
Page::set('Profile/LK/EditImage');
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -35,6 +35,8 @@ class Routes
|
|||
Router::get('/api/photo/loadrecent', 'ApiController@recentphotos');
|
||||
Router::get('/api/users/load/$id', 'ApiController@loaduser');
|
||||
Router::get('/api/users/emailverify', 'ApiController@emailverify');
|
||||
Router::get('/api/users/search', 'ApiController@userssearch');
|
||||
Router::get('/api/emoji/load', 'ApiController@emojiload');
|
||||
Router::get('/article/$id', 'MainController@gallery');
|
||||
Router::get('/voting', 'ContestsController@index');
|
||||
Router::get('/voting/results', 'ContestsController@results');
|
||||
|
@ -46,13 +48,18 @@ class Routes
|
|||
Router::get('/lk', 'ProfileController@lk');
|
||||
Router::get('/lk/upload', 'ProfileController@upload');
|
||||
Router::get('/lk/history', 'ProfileController@lkhistory');
|
||||
Router::get('/lk/profile', 'ProfileController@lkprofile');
|
||||
Router::any('/lk/profile', 'ProfileController@lkprofile');
|
||||
Router::get('/lk/pday', 'ProfileController@photoindexhistory');
|
||||
Router::get('/lk/editimage', 'ProfileController@editimage');
|
||||
Router::get('/fav_authors', 'MainController@favauthors');
|
||||
|
||||
Router::get('/messages', 'MessagesController@i');
|
||||
|
||||
|
||||
Router::get('/search', 'SearchController@i');
|
||||
|
||||
Router::get('/fav', 'MainController@fav');
|
||||
Router::get('/mapmedia', 'MainController@mapmedia');
|
||||
Router::get('/voting/sendpretend', 'ContestsController@sendpretend');
|
||||
|
||||
Router::get('/vehicle/edit', 'VehicleController@iedit');
|
||||
|
@ -60,6 +67,7 @@ class Routes
|
|||
Router::post('/api/upload', 'ApiController@upload');
|
||||
Router::post('/api/profile/update', 'ApiController@updateprofile');
|
||||
Router::post('/api/photo/comment', 'ApiController@photocomment');
|
||||
Router::get('/api/photo/loadmap', 'ApiController@photoloadmap');
|
||||
Router::get('/api/photo/$id/favorite', 'ApiController@photofavorite');
|
||||
Router::get('/api/subscribe', 'ApiController@subscribeuser');
|
||||
Router::post('/api/photo/getcomments/$id', 'ApiController@photocommentload');
|
||||
|
@ -72,6 +80,10 @@ class Routes
|
|||
Router::post('/api/photo/contests/sendpretend', 'ApiController@sendpretendphoto');
|
||||
Router::get('/api/photo/contests/rate', 'ApiController@photovotecontest');
|
||||
Router::get('/api/contests/getinfo', 'ApiController@contestsgetinfo');
|
||||
Router::any('/api/messages/getchats', 'ApiController@msggetchats');
|
||||
Router::any('/api/messages/upload', 'ApiController@msgupload');
|
||||
Router::any('/api/messages/getusers', 'ApiController@msggetusers');
|
||||
Router::any('/api/messages/createchat', 'ApiController@msgcreatechat');
|
||||
Router::get('/api/vehicles/load', 'ApiController@vehiclesload');
|
||||
Router::get('/api/geodb/search', 'ApiController@geodbsearch');
|
||||
if ($user->i('admin') > 0) {
|
||||
|
@ -93,4 +105,4 @@ class Routes
|
|||
}
|
||||
Router::get('/vehicle/$id', 'VehicleController@i');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use \App\Services\{DB, Date, Auth};
|
||||
use \App\Services\{DB, Date, Auth, Emoji, Word};
|
||||
use \App\Models\{User, Photo, Vote};
|
||||
|
||||
class Comment
|
||||
|
@ -24,6 +24,123 @@ class Comment
|
|||
$content = json_decode($this->c['content'], true);
|
||||
return $content[$table];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private function processContent($rawText)
|
||||
{
|
||||
// 1. Обработка упоминаний и смайлов
|
||||
$withTags = Emoji::parseSmileys(Word::processMentions($rawText));
|
||||
|
||||
// 2. Селективное экранирование
|
||||
$safeContent = $this->selectiveHtmlEscape($withTags);
|
||||
|
||||
// 3. Обрезка контента
|
||||
return $this->truncateContent($safeContent, 200);
|
||||
}
|
||||
|
||||
private function selectiveHtmlEscape(string $html): string
|
||||
{
|
||||
// 0. Если текст не UTF‑8, конвертируем из CP1251
|
||||
if (!mb_check_encoding($html, 'UTF-8')) {
|
||||
$html = mb_convert_encoding($html, 'UTF-8', 'CP1251');
|
||||
}
|
||||
|
||||
// 1. Разбиваем на «теги» и «текст», сохраняя теги
|
||||
$parts = preg_split('/(<[^>]+>)/u', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
foreach ($parts as &$part) {
|
||||
// 2. Тег — пропускаем
|
||||
if (preg_match('/^<[^>]+>$/u', $part)) {
|
||||
continue;
|
||||
}
|
||||
// 3. Текст — сначала декодируем все сущности, потом экранируем спецсимволы
|
||||
// ENT_QUOTES|ENT_HTML5 и false у double_encode гарантируют корректную работу с etc.
|
||||
$decoded = html_entity_decode($part, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$part = htmlspecialchars($decoded, ENT_QUOTES | ENT_HTML5, 'UTF-8', false);
|
||||
}
|
||||
unset($part);
|
||||
|
||||
// 4. Собираем обратно
|
||||
return implode('', $parts);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private function truncateContent(string $html, int $maxLength): string
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$wrapped = '<?xml encoding="UTF-8"><div>' . $html . '</div>';
|
||||
|
||||
$dom->loadHTML($wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$node = $xpath->query('//div')->item(0);
|
||||
$this->truncateNode($node, $maxLength);
|
||||
|
||||
|
||||
return $dom->saveHTML($node);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function truncateNode(\DOMNode $node, &$remaining)
|
||||
{
|
||||
if ($remaining <= 0) return;
|
||||
|
||||
foreach ($node->childNodes as $child) {
|
||||
if ($child instanceof \DOMText) {
|
||||
$text = $child->nodeValue;
|
||||
$visible = mb_substr($text, 0, $remaining);
|
||||
$hidden = mb_substr($text, $remaining);
|
||||
|
||||
if ($remaining < mb_strlen($text)) {
|
||||
$child->nodeValue = $visible;
|
||||
$remaining = 0;
|
||||
|
||||
// Создаём элемент для скрытой части
|
||||
$hiddenNode = $child->ownerDocument->createElement('span');
|
||||
$hiddenNode->setAttribute('class', 'hidden-text');
|
||||
$hiddenTextNode = $child->ownerDocument->createTextNode($hidden);
|
||||
$hiddenNode->appendChild($hiddenTextNode);
|
||||
|
||||
// Вставляем hiddenNode после текущего текстового узла
|
||||
$parent = $child->parentNode;
|
||||
if ($parent) {
|
||||
if ($child->nextSibling) {
|
||||
$parent->insertBefore($hiddenNode, $child->nextSibling);
|
||||
} else {
|
||||
$parent->appendChild($hiddenNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Создаём кнопку "показать больше"
|
||||
$button = $child->ownerDocument->createElement('a');
|
||||
$buttonText = $child->ownerDocument->createTextNode('показать больше');
|
||||
$button->appendChild($buttonText);
|
||||
$button->setAttribute('class', 'toggle-message');
|
||||
if ($parent) {
|
||||
$parent->appendChild($button);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$remaining -= mb_strlen($text);
|
||||
} else {
|
||||
$this->truncateNode($child, $remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function i()
|
||||
{
|
||||
$user = new User($this->c['user_id']);
|
||||
|
@ -39,8 +156,6 @@ class Comment
|
|||
echo '
|
||||
<div style="float:right; text-align:right" class="sm">
|
||||
<span class="message_date">' . Date::zmdate($this->c['posted_at']) . '</span><br>
|
||||
<a href="#" class="quoteLink dot">Цитировать</a>
|
||||
·
|
||||
<a href="#' . $this->c['id'] . '" class="cmLink dot">Ссылка</a>
|
||||
';
|
||||
|
||||
|
@ -73,19 +188,31 @@ class Comment
|
|||
$commclass = '';
|
||||
}
|
||||
echo '</span></div>
|
||||
<div class="rank">Фото: ' . Photo::fetchAll($this->c['user_id']) . ' ' . $admintype . '</div>
|
||||
<div class="message-text">' . preg_replace("~(?:[\p{M}]{1})([\p{M}])+?~uis", "", htmlspecialchars($this->c['body'])) . '</div>
|
||||
';
|
||||
if ($content['filetype'] === 'img') {
|
||||
echo '<div class="message-text"><img src="'.$content['src'].'" width="250"></div>';
|
||||
}
|
||||
if ($content['filetype'] === 'video') {
|
||||
echo '<div class="message-text"><video controls src="'.$content['src'].'" width="250"></div>';
|
||||
}
|
||||
echo '
|
||||
<div class="rank">Фото: ' . Photo::fetchAll($this->c['user_id']) . ' ' . $admintype . '</div>'; ?>
|
||||
<div class="message-text">
|
||||
<?php
|
||||
// Правильный порядок:
|
||||
$processedText = $this->processContent($this->c['body']);
|
||||
|
||||
// Шаг 4: Вывод без дополнительного экранирования
|
||||
echo '<div class="message-text">' . $processedText . '</div>';
|
||||
|
||||
// ========== Вспомогательные методы ==========
|
||||
|
||||
|
||||
?>
|
||||
</div> <?php
|
||||
|
||||
if ($content['filetype'] === 'img') {
|
||||
echo '<div class="message-text"><img src="' . $content['src'] . '" width="250"></div>';
|
||||
}
|
||||
if ($content['filetype'] === 'video') {
|
||||
echo '<div class="message-text"><video controls src="' . $content['src'] . '" width="250"></div>';
|
||||
}
|
||||
echo '
|
||||
<div class="comment-votes-block">
|
||||
';
|
||||
echo '<style>
|
||||
echo '<style>
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
@ -105,28 +232,28 @@ class Comment
|
|||
display: block;
|
||||
}
|
||||
</style>';
|
||||
if ($this->c['user_id'] === Auth::userid() || $photo->i('user_id') === Auth::userid()) {
|
||||
echo '
|
||||
if ($this->c['user_id'] === Auth::userid() || $photo->i('user_id') === Auth::userid()) {
|
||||
echo '
|
||||
<div class="dropdown">
|
||||
<a style="color: #000" class="compl" href="#">...</a>
|
||||
<div class="dropdown-content">';
|
||||
|
||||
|
||||
?>
|
||||
|
||||
<a href="#" onclick="pinComment(<?= $this->c['id'] ?>); return false;"><?=$pinc?></a><br>
|
||||
<?php
|
||||
if ($this->c['user_id'] === Auth::userid()) { ?>
|
||||
<a style="margin-bottom: 10px;" href="#" onclick="createModal(<?= $this->c['id'] ?>, 'EDIT_COMMENT', '<?= htmlspecialchars($this->c['body']) ?>', 'modaledit<?= $this->c['id'] ?>'); return false;">Редактировать</a><br>
|
||||
<a href="#" onclick="createModal(<?= $this->c['id'] ?>, 'DELETE_COMMENT', '', 'modaldel<?= $this->c['id'] ?>'); return false;">Удалить</a>
|
||||
<?php }
|
||||
|
||||
echo '
|
||||
|
||||
?>
|
||||
|
||||
<a href="#" onclick="pinComment(<?= $this->c['id'] ?>); return false;"><?= $pinc ?></a><br>
|
||||
<?php
|
||||
if ($this->c['user_id'] === Auth::userid()) { ?>
|
||||
<a style="margin-bottom: 10px;" href="#" onclick="createModal(<?= $this->c['id'] ?>, 'EDIT_COMMENT', '<?= htmlspecialchars($this->c['body']) ?>', 'modaledit<?= $this->c['id'] ?>'); return false;">Редактировать</a><br>
|
||||
<a href="#" onclick="createModal(<?= $this->c['id'] ?>, 'DELETE_COMMENT', '', 'modaldel<?= $this->c['id'] ?>'); return false;">Удалить</a>
|
||||
<?php }
|
||||
|
||||
echo '
|
||||
</div>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
echo '
|
||||
}
|
||||
echo '
|
||||
<div class="wvote" wid="' . $this->c['id'] . '">
|
||||
<a href="#" vote="1" class="w-btn s2"><span>+</span></a>
|
||||
|
||||
|
@ -139,5 +266,5 @@ class Comment
|
|||
</div>
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,5 +15,14 @@ class User {
|
|||
$content = json_decode(self::i('content'), true);
|
||||
return $content[$table];
|
||||
}
|
||||
public function getPhotoUrl(): string
|
||||
{
|
||||
return $this->i('photourl');
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return (int)$this->i('user_id');
|
||||
}
|
||||
|
||||
}
|
|
@ -1,49 +1,241 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use \PDO;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use RuntimeException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class DB {
|
||||
private static $pdoInstance = null;
|
||||
private static $cache = [];
|
||||
class DB
|
||||
{
|
||||
const DRIVERS = ['mysql', 'pgsql', 'sqlite'];
|
||||
|
||||
public static function connect() {
|
||||
if (self::$pdoInstance === null) {
|
||||
$dsn = 'mysql:host='.NGALLERY['root']['db']['host'].';dbname='.NGALLERY['root']['db']['name'].';charset=utf8mb4';
|
||||
$username = NGALLERY['root']['db']['login'];
|
||||
$password = NGALLERY['root']['db']['password'];
|
||||
private static $config = [];
|
||||
private static $queryLog = [];
|
||||
private static $logger;
|
||||
private static $connectionPool = [];
|
||||
private static $poolSize = 5;
|
||||
|
||||
try {
|
||||
self::$pdoInstance = new PDO($dsn, $username, $password);
|
||||
self::$pdoInstance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $ex) {
|
||||
die("Connection failed: " . $ex->getMessage());
|
||||
}
|
||||
/* Инициализация и конфигурация */
|
||||
public static function init(array $config): void
|
||||
{
|
||||
self::validateConfig($config);
|
||||
self::$config = array_merge([
|
||||
'driver' => 'mysql',
|
||||
'host' => 'localhost',
|
||||
'database' => '',
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'prefix' => '',
|
||||
'log_file' => null,
|
||||
'cache' => null,
|
||||
'benchmark' => false,
|
||||
'pool_size' => 5
|
||||
], $config);
|
||||
|
||||
self::$poolSize = self::$config['pool_size'];
|
||||
|
||||
if (self::$config['log_file']) {
|
||||
self::$logger = new class(self::$config['log_file']) {
|
||||
private $file;
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
$this->file = fopen($path, 'a');
|
||||
}
|
||||
|
||||
public function log(string $message): void
|
||||
{
|
||||
fwrite($this->file, date('[Y-m-d H:i:s] ') . $message . PHP_EOL);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
fclose($this->file);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return self::$pdoInstance;
|
||||
}
|
||||
|
||||
public static function query($query, $params = array(), $useCache = false) {
|
||||
if ($useCache && isset(self::$cache[$query])) {
|
||||
return self::$cache[$query];
|
||||
public static function lastInsertId() {
|
||||
return self::getConnection()->lastInsertId();
|
||||
}
|
||||
|
||||
private static function validateConfig(array $config): void
|
||||
{
|
||||
if (!in_array($config['driver'], self::DRIVERS)) {
|
||||
throw new InvalidArgumentException('Invalid database driver');
|
||||
}
|
||||
}
|
||||
|
||||
/* Пул соединений */
|
||||
private static function getConnection(): PDO
|
||||
{
|
||||
if (!empty(self::$connectionPool)) {
|
||||
return array_shift(self::$connectionPool);
|
||||
}
|
||||
|
||||
$statement = self::connect()->prepare($query);
|
||||
$dsn = match (self::$config['driver']) {
|
||||
'mysql' => "mysql:host=" . self::$config['host'] . ";dbname=" . self::$config['database'] . ";charset=utf8mb4",
|
||||
'pgsql' => "pgsql:host=" . self::$config['host'] . ";dbname=" . self::$config['database'] . "",
|
||||
'sqlite' => "sqlite:" . self::$config['database'] . ""
|
||||
};
|
||||
|
||||
try {
|
||||
$statement->execute($params);
|
||||
$pdo = new PDO($dsn, self::$config['username'], self::$config['password'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_PERSISTENT => true
|
||||
]);
|
||||
|
||||
if (explode(' ', $query)[0] === 'SELECT' || explode(' ', $query)[0] === 'SHOW' || explode(' ', $query)[0] === 'DESCRIBE') {
|
||||
$data = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($useCache) {
|
||||
self::$cache[$query] = $data;
|
||||
}
|
||||
return $data;
|
||||
if (self::$config['driver'] === 'mysql') {
|
||||
$pdo->exec("SET SQL_MODE='STRICT_ALL_TABLES'");
|
||||
}
|
||||
} catch (PDOException $ex) {
|
||||
die("Query failed: " . $ex->getMessage());
|
||||
|
||||
return $pdo;
|
||||
} catch (PDOException $e) {
|
||||
self::logError($e);
|
||||
throw new RuntimeException("Connection failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static function releaseConnection(PDO $connection): void
|
||||
{
|
||||
if (count(self::$connectionPool) < self::$poolSize) {
|
||||
self::$connectionPool[] = $connection;
|
||||
}
|
||||
}
|
||||
|
||||
/* Основные методы */
|
||||
public static function query(string $sql, array $params = []): array
|
||||
{
|
||||
$start = microtime(true);
|
||||
$conn = self::getConnection();
|
||||
|
||||
try {
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (self::$config['benchmark']) {
|
||||
self::$queryLog[] = [
|
||||
'query' => $sql,
|
||||
'params' => $params,
|
||||
'time' => microtime(true) - $start
|
||||
];
|
||||
}
|
||||
|
||||
self::releaseConnection($conn);
|
||||
return $result;
|
||||
} catch (PDOException $e) {
|
||||
self::logError($e);
|
||||
throw new RuntimeException("Query failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/* Транзакции */
|
||||
public static function transaction(callable $callback)
|
||||
{
|
||||
$conn = self::getConnection();
|
||||
try {
|
||||
$conn->beginTransaction();
|
||||
$result = $callback();
|
||||
$conn->commit();
|
||||
self::releaseConnection($conn);
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
$conn->rollBack();
|
||||
self::releaseConnection($conn);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Построитель запросов */
|
||||
public static function table(string $table): QueryBuilder
|
||||
{
|
||||
return new QueryBuilder(
|
||||
self::$config['prefix'] . $table,
|
||||
self::$config['driver']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static function logError(\Throwable $e): void
|
||||
{
|
||||
if (self::$logger) {
|
||||
self::$logger->log("ERROR: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
class QueryBuilder
|
||||
{
|
||||
private $wheres = [];
|
||||
private $joins = [];
|
||||
private $columns = ['*'];
|
||||
private $bindings = [];
|
||||
|
||||
public function __construct(
|
||||
private string $table,
|
||||
private string $driver
|
||||
) {
|
||||
// Добавляем проверку
|
||||
if (empty($this->driver)) {
|
||||
throw new RuntimeException('Database driver not configured');
|
||||
}
|
||||
}
|
||||
|
||||
public function select(array $columns): self
|
||||
{
|
||||
$this->columns = $columns;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function join(string $table, string $first, string $operator, string $second, string $type = 'INNER'): self
|
||||
{
|
||||
$this->joins[] = "$type JOIN $table ON $first $operator $second";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function where(string $column, string $operator, $value): self
|
||||
{
|
||||
$param = 'where_' . count($this->bindings);
|
||||
$this->wheres[] = "$column $operator :$param";
|
||||
$this->bindings[$param] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get(): array
|
||||
{
|
||||
$sql = "SELECT " . implode(', ', $this->columns) . " FROM $this->table";
|
||||
|
||||
if (!empty($this->joins)) {
|
||||
$sql .= " " . implode(' ', $this->joins);
|
||||
}
|
||||
|
||||
if (!empty($this->wheres)) {
|
||||
$sql .= " WHERE " . implode(' AND ', $this->wheres);
|
||||
}
|
||||
|
||||
return DB::query($sql, $this->bindings);
|
||||
}
|
||||
|
||||
public function create(array $columns): void
|
||||
{
|
||||
$definitions = [];
|
||||
foreach ($columns as $name => $type) {
|
||||
$definitions[] = "$name $type";
|
||||
}
|
||||
|
||||
$sql = "CREATE TABLE $this->table (" . implode(', ', $definitions) . ")";
|
||||
DB::query($sql);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
77
app/Services/Emoji.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
|
||||
class Emoji
|
||||
{
|
||||
|
||||
public static function parseSmileys($text) {
|
||||
return preg_replace_callback(
|
||||
'/\[(\d+\/[\w-]+)\]/',
|
||||
function($matches) {
|
||||
$parts = explode('/', $matches[1]);
|
||||
$dir = $parts[0];
|
||||
$name = $parts[1];
|
||||
|
||||
$files = glob($_SERVER['DOCUMENT_ROOT']."/static/img/smileys/$dir/$name.*");
|
||||
|
||||
if ($files) {
|
||||
$ext = pathinfo($files[0], PATHINFO_EXTENSION);
|
||||
return "<img src='/static/img/smileys/$dir/$name.$ext'
|
||||
class='emoji'
|
||||
data-code='".$matches[0]."'>";
|
||||
}
|
||||
|
||||
return htmlspecialchars($matches[0]);
|
||||
},
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
public static function getAllSmileys() {
|
||||
// Пример реализации для файловой системы
|
||||
$smileys = [];
|
||||
$directories = glob($_SERVER['DOCUMENT_ROOT'].'/static/img/smileys/*', GLOB_ONLYDIR);
|
||||
|
||||
foreach ($directories as $dir) {
|
||||
$dirName = basename($dir);
|
||||
$files = glob($dir.'/*.{gif,png,jpg,webp}', GLOB_BRACE);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filename = pathinfo($file, PATHINFO_FILENAME);
|
||||
$ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||
$code = "[{$dirName}/{$filename}]";
|
||||
|
||||
$smileys[] = [
|
||||
'code' => $code,
|
||||
'url' => "/static/img/smileys/{$dirName}/{$filename}.{$ext}"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $smileys;
|
||||
}
|
||||
|
||||
|
||||
public static function expandSmileys($content)
|
||||
{
|
||||
$pattern = '/\[([0-9]+\/[a-zA-Z0-9_-]+)\]/';
|
||||
|
||||
return preg_replace_callback($pattern, function ($matches) {
|
||||
$path = explode('/', $matches[1]);
|
||||
$dir = $path[0];
|
||||
$name = $path[1];
|
||||
|
||||
$files = glob($_SERVER['DOCUMENT_ROOT'] . "/static/img/smileys/{$dir}/{$name}.*");
|
||||
|
||||
if (count($files) > 0) {
|
||||
$file = basename($files[0]);
|
||||
return "<img src=\"/static/img/smileys/{$dir}/{$file}\" " .
|
||||
"class=\"editor-emoji\" data-code=\"{$matches[0]}\">";
|
||||
}
|
||||
|
||||
return $matches[0];
|
||||
}, $content);
|
||||
}
|
||||
}
|
272
app/Services/Image.php
Normal file
|
@ -0,0 +1,272 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Exception;
|
||||
use GdImage;
|
||||
|
||||
class Image
|
||||
{
|
||||
private const CACHE_DIR = __DIR__ . '/../../cdn/';
|
||||
private const LOCK_DIR = __DIR__ . '/../../storage/locks/';
|
||||
private const QUEUE_FILE = __DIR__ . '/../../storage/queue/image_processing.queue';
|
||||
private const MAX_FILE_SIZE = 5242880; // 5MB
|
||||
|
||||
public static function generateBlurredPlaceholder(string $imageUrl, int $quality = 30): string
|
||||
{
|
||||
try {
|
||||
self::checkDirectories();
|
||||
$cacheFile = self::CACHE_DIR . md5($imageUrl) . '.jpg';
|
||||
|
||||
if (self::isValidCache($cacheFile)) {
|
||||
return self::getCachedImage($cacheFile);
|
||||
}
|
||||
|
||||
if (!self::isProcessing($imageUrl)) {
|
||||
self::addToQueue($imageUrl, $quality);
|
||||
error_log("Added to queue: " . $imageUrl);
|
||||
}
|
||||
|
||||
return self::getTransparentPixel();
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in generateBlurredPlaceholder: " . $e->getMessage());
|
||||
return self::getTransparentPixel();
|
||||
}
|
||||
}
|
||||
|
||||
private static function checkDirectories(): void
|
||||
{
|
||||
try {
|
||||
$dirs = [
|
||||
self::CACHE_DIR,
|
||||
self::LOCK_DIR,
|
||||
dirname(self::QUEUE_FILE)
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if (!file_exists($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
error_log("Created directory: $dir");
|
||||
}
|
||||
|
||||
// Проверяем права записи
|
||||
if (!is_writable($dir)) {
|
||||
throw new Exception("Directory not writable: $dir");
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем файл очереди
|
||||
if (!file_exists(self::QUEUE_FILE)) {
|
||||
touch(self::QUEUE_FILE);
|
||||
chmod(self::QUEUE_FILE, 0666);
|
||||
error_log("Created queue file: " . self::QUEUE_FILE);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("Directory error: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function processQueue(): void
|
||||
{
|
||||
self::checkDirectories();
|
||||
if (!file_exists(self::QUEUE_FILE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queue = file(self::QUEUE_FILE, FILE_IGNORE_NEW_LINES);
|
||||
foreach ($queue as $line) {
|
||||
try {
|
||||
[$hash, $data] = explode('|', $line, 2);
|
||||
$task = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
self::processImageTask($task['url'], $task['quality']);
|
||||
self::removeFromQueue($hash);
|
||||
} catch (Exception $e) {
|
||||
error_log('Queue processing error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function processImageTask(string $imageUrl, int $quality): void
|
||||
{
|
||||
$cacheFile = self::CACHE_DIR . md5($imageUrl) . '.jpg';
|
||||
$lockFile = self::LOCK_DIR . md5($imageUrl) . '.lock';
|
||||
|
||||
$lockHandle = fopen($lockFile, 'w');
|
||||
if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$imageData = self::fetchImage($imageUrl);
|
||||
$processedImage = self::createBlurredImage($imageData, $quality);
|
||||
file_put_contents($cacheFile, $processedImage);
|
||||
} finally {
|
||||
flock($lockHandle, LOCK_UN);
|
||||
fclose($lockHandle);
|
||||
@unlink($lockFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static function createBlurredImage(string $imageData, int $quality): string
|
||||
{
|
||||
$tempFile = tmpfile();
|
||||
try {
|
||||
fwrite($tempFile, $imageData);
|
||||
$tempPath = stream_get_meta_data($tempFile)['uri'];
|
||||
|
||||
$img = self::createImageResource($tempPath);
|
||||
$scaled = self::scaleAndBlurImage($img);
|
||||
|
||||
ob_start();
|
||||
imagejpeg($scaled, null, $quality);
|
||||
$contents = ob_get_clean();
|
||||
|
||||
if (empty($contents)) {
|
||||
throw new Exception('JPEG generation failed');
|
||||
}
|
||||
|
||||
return $contents;
|
||||
} finally {
|
||||
if (isset($img) && $img instanceof GdImage) {
|
||||
imagedestroy($img);
|
||||
}
|
||||
if (isset($scaled) && $scaled instanceof GdImage) {
|
||||
imagedestroy($scaled);
|
||||
}
|
||||
fclose($tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static function createImageResource(string $path): GdImage
|
||||
{
|
||||
$mime = (new \finfo(FILEINFO_MIME_TYPE))->file($path);
|
||||
|
||||
return match ($mime) {
|
||||
'image/jpeg' => imagecreatefromjpeg($path),
|
||||
'image/png' => self::createTrueColorPng($path),
|
||||
'image/gif' => imagecreatefromgif($path),
|
||||
default => throw new Exception("Unsupported MIME type: $mime")
|
||||
};
|
||||
}
|
||||
|
||||
private static function createTrueColorPng(string $path): GdImage
|
||||
{
|
||||
$img = imagecreatefrompng($path);
|
||||
if (!imageistruecolor($img)) {
|
||||
imagepalettetotruecolor($img);
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
private static function scaleAndBlurImage(GdImage $img): GdImage
|
||||
{
|
||||
$scaled = imagescale($img, 10, 10, IMG_BICUBIC);
|
||||
if (!$scaled || !imagefilter($scaled, IMG_FILTER_GAUSSIAN_BLUR)) {
|
||||
throw new Exception('Image processing failed');
|
||||
}
|
||||
return $scaled;
|
||||
}
|
||||
|
||||
private static function fetchImage(string $imageUrl): string
|
||||
{
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 5,
|
||||
'header' => "Range: bytes=0-" . self::MAX_FILE_SIZE
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false
|
||||
]
|
||||
]);
|
||||
|
||||
$imageData = file_get_contents($imageUrl, false, $context);
|
||||
if ($imageData === false) {
|
||||
throw new Exception("Failed to download image");
|
||||
}
|
||||
return $imageData;
|
||||
}
|
||||
|
||||
private static function isValidCache(string $cacheFile): bool
|
||||
{
|
||||
return file_exists($cacheFile) && filesize($cacheFile) > 0;
|
||||
}
|
||||
|
||||
|
||||
private static function isProcessing(string $imageUrl): bool
|
||||
{
|
||||
return
|
||||
file_exists(self::LOCK_DIR . md5($imageUrl) . '.lock') ||
|
||||
self::isInQueue(md5($imageUrl));
|
||||
}
|
||||
|
||||
|
||||
private static function isInQueue(string $fileHash): bool
|
||||
{
|
||||
if (!file_exists(self::QUEUE_FILE)) return false;
|
||||
|
||||
$handle = fopen(self::QUEUE_FILE, 'r');
|
||||
if (!$handle) return false;
|
||||
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
if (strpos($line, $fileHash) === 0) {
|
||||
fclose($handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static function addToQueue(string $imageUrl, int $quality): void
|
||||
{
|
||||
$data = [
|
||||
'url' => $imageUrl,
|
||||
'quality' => $quality,
|
||||
'created_at' => time()
|
||||
];
|
||||
|
||||
$queueEntry = md5($imageUrl) . '|' . json_encode($data);
|
||||
|
||||
if (file_put_contents(
|
||||
self::QUEUE_FILE,
|
||||
$queueEntry . PHP_EOL,
|
||||
FILE_APPEND | LOCK_EX
|
||||
) === false) {
|
||||
throw new Exception("Failed to write to queue file");
|
||||
}
|
||||
}
|
||||
|
||||
private static function removeFromQueue(string $fileHash): void
|
||||
{
|
||||
$queue = file(self::QUEUE_FILE, FILE_IGNORE_NEW_LINES);
|
||||
$newQueue = [];
|
||||
|
||||
foreach ($queue as $line) {
|
||||
if (!str_starts_with($line, $fileHash)) {
|
||||
$newQueue[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents(self::QUEUE_FILE, implode(PHP_EOL, $newQueue));
|
||||
}
|
||||
|
||||
|
||||
private static function getCachedImage(string $cacheFile): string
|
||||
{
|
||||
$content = file_get_contents($cacheFile);
|
||||
if ($content === false) {
|
||||
throw new Exception("Failed to read cached image");
|
||||
}
|
||||
return 'data:image/jpeg;base64,' . base64_encode($content);
|
||||
}
|
||||
|
||||
private static function getTransparentPixel(): string
|
||||
{
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBgYAAAAAQAAHpQoNMAAAAASUVORK5CYII=';
|
||||
}
|
||||
}
|
|
@ -7,16 +7,26 @@ use \App\Core\Page;
|
|||
class Router
|
||||
{
|
||||
|
||||
protected static $routes = [];
|
||||
|
||||
|
||||
private static function addRoute($method, $route)
|
||||
{
|
||||
self::$routes[] = [
|
||||
'method' => $method,
|
||||
'path' => $route
|
||||
];
|
||||
}
|
||||
public static function get($route, $path_to_include)
|
||||
{
|
||||
self::addRoute('GET', $route);
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
|
||||
self::route($route, $path_to_include);
|
||||
}
|
||||
}
|
||||
public static function post($route, $path_to_include)
|
||||
{
|
||||
self::addRoute('POST', $route);
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
self::route($route, $path_to_include);
|
||||
}
|
||||
|
@ -38,9 +48,23 @@ class Router
|
|||
if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
|
||||
self::route($route, $path_to_include);
|
||||
}
|
||||
}
|
||||
public static function getRouteSegments()
|
||||
{
|
||||
$segments = [];
|
||||
foreach (self::$routes as $route) {
|
||||
$parts = explode('/', $route['path']);
|
||||
foreach ($parts as $part) {
|
||||
if (!empty($part) && !str_starts_with($part, '$')) {
|
||||
$segments[] = $part;
|
||||
}
|
||||
}
|
||||
}
|
||||
return array_unique($segments);
|
||||
}
|
||||
public static function any($route, $path_to_include)
|
||||
{
|
||||
self::addRoute('ANY', $route);
|
||||
self::route($route, $path_to_include);
|
||||
}
|
||||
public static function route($route, $path_to_include)
|
||||
|
|
143
app/Services/ThemeManager.php
Normal file
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use RuntimeException, InvalidArgumentException;
|
||||
|
||||
|
||||
class ThemeManager
|
||||
{
|
||||
private $themepacksPath;
|
||||
private $loadedThemes = [];
|
||||
private $defaultThemeId = 'standard';
|
||||
|
||||
public function __construct(?string $themepacksPath = null)
|
||||
{
|
||||
$this->themepacksPath = $themepacksPath ?? $_SERVER['DOCUMENT_ROOT'] . '/static/themepacks';
|
||||
}
|
||||
|
||||
public function loadThemes(): void
|
||||
{
|
||||
if (!is_dir($this->themepacksPath)) return;
|
||||
|
||||
$folders = array_diff(scandir($this->themepacksPath), ['..', '.']);
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$themePath = $this->themepacksPath . '/' . $folder;
|
||||
$configFile = $themePath . '/theme.yaml';
|
||||
$cssFile = $themePath . '/root.css';
|
||||
if (is_dir($themePath)) {
|
||||
try {
|
||||
if (!file_exists($configFile)) {
|
||||
throw new RuntimeException("Missing theme.yaml in {$folder}");
|
||||
}
|
||||
if (!file_exists($cssFile)) {
|
||||
throw new RuntimeException("Missing root.css in {$folder}");
|
||||
}
|
||||
|
||||
$config = Yaml::parseFile($configFile);
|
||||
$this->validateThemeConfig($config, $folder);
|
||||
|
||||
$this->loadedThemes[$folder] = [
|
||||
'id' => $folder,
|
||||
'config' => $config,
|
||||
'stylesheet' => '/static/themepacks/' . $folder . '/root.css'
|
||||
];
|
||||
} catch (ParseException | RuntimeException $e) {
|
||||
error_log("Theme load error ({$folder}): " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getAllThemes(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->loadedThemes as $theme) {
|
||||
$result[] = [
|
||||
'id' => $theme['id'],
|
||||
'name' => $theme['config']['name'],
|
||||
'version' => $theme['config']['version'],
|
||||
'author' => $theme['config']['author'],
|
||||
'supported_nativegallery' => $theme['config']['supported_nativegallery'],
|
||||
'stylesheet' => $theme['stylesheet']
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getThemeStylesheet(): ?string
|
||||
{
|
||||
$this->startSession();
|
||||
|
||||
$themeId = $_SESSION['selected_theme'] ?? $this->defaultThemeId;
|
||||
|
||||
if ($themeId === $this->defaultThemeId || !isset($this->loadedThemes[$themeId])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->loadedThemes[$themeId]['stylesheet'];
|
||||
}
|
||||
|
||||
public function saveThemeToProfile(string $themeId): bool
|
||||
{
|
||||
if ($themeId !== $this->defaultThemeId && !isset($this->loadedThemes[$themeId])) {
|
||||
throw new InvalidArgumentException("Theme {$themeId} not found");
|
||||
}
|
||||
|
||||
$this->startSession();
|
||||
$_SESSION['selected_theme'] = $themeId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getAvailableThemes(): array
|
||||
{
|
||||
return $this->loadedThemes;
|
||||
}
|
||||
|
||||
private function validateThemeConfig(array $config, string $folder): void
|
||||
{
|
||||
$requiredFields = [
|
||||
'name',
|
||||
'version',
|
||||
'supported_nativegallery',
|
||||
'author'
|
||||
];
|
||||
|
||||
foreach ($requiredFields as $field) {
|
||||
if (!isset($config[$field])) {
|
||||
throw new RuntimeException(
|
||||
"Missing required field '{$field}' in theme: {$folder}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getCurrentThemeId(): string {
|
||||
$this->startSession();
|
||||
return $_SESSION['selected_theme'] ?? $this->defaultThemeId;
|
||||
}
|
||||
|
||||
public function getCurrentThemeName(): string {
|
||||
$themeId = $this->getCurrentThemeId();
|
||||
return $themeId === $this->defaultThemeId
|
||||
? 'Стандартная'
|
||||
: $this->loadedThemes[$themeId]['config']['name'] ?? 'Неизвестная тема';
|
||||
}
|
||||
|
||||
public function getThemeNameById(string $themeId): ?string
|
||||
{
|
||||
return $this->loadedThemes[$themeId]['config']['name'] ?? null;
|
||||
}
|
||||
|
||||
private function startSession(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,17 +10,18 @@ class Upload
|
|||
public $src;
|
||||
public $size;
|
||||
public $name;
|
||||
public $previewUrl;
|
||||
|
||||
private static function human_filesize($bytes, $dec = 2): string
|
||||
{
|
||||
{
|
||||
|
||||
$size = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
|
||||
$factor = floor((strlen($bytes) - 1) / 3);
|
||||
if ($factor == 0)
|
||||
$dec = 0;
|
||||
$size = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
|
||||
$factor = floor((strlen($bytes) - 1) / 3);
|
||||
if ($factor == 0)
|
||||
$dec = 0;
|
||||
|
||||
|
||||
return sprintf("%.{$dec}f %s", $bytes / (1024 ** $factor), $size[$factor]);
|
||||
return sprintf("%.{$dec}f %s", $bytes / (1024 ** $factor), $size[$factor]);
|
||||
}
|
||||
public function __construct($file, $location)
|
||||
{
|
||||
|
@ -39,14 +40,13 @@ class Upload
|
|||
$filecdn = bin2hex(openssl_random_pseudo_bytes(64, $cstrong)) . '.' . $fileext;
|
||||
$folder = $location . $filecdn;
|
||||
|
||||
if (strtolower (NGALLERY['root']['storage']['type']) == "s3")
|
||||
{
|
||||
if (strtolower(NGALLERY['root']['storage']['type']) == "s3") {
|
||||
|
||||
if (NGALLERY['root']['video']['upload']['cloudflare-bypass'] === true) {
|
||||
if ($location === 'cdn/video') {
|
||||
if (filesize($_SERVER['DOCUMENT_ROOT'].'/'.$location.$filecdn) >= 94371840) {
|
||||
if (filesize($_SERVER['DOCUMENT_ROOT'] . '/' . $location . $filecdn) >= 94371840) {
|
||||
mkdir("{$_SERVER['DOCUMENT_ROOT']}/uploads/{$location}", 0777, true);
|
||||
move_uploaded_file ($tmpname, "{$_SERVER['DOCUMENT_ROOT']}/uploads/{$folder}");
|
||||
move_uploaded_file($tmpname, "{$_SERVER['DOCUMENT_ROOT']}/uploads/{$folder}");
|
||||
$this->type = $type;
|
||||
$this->src = "/uploads/{$folder}";
|
||||
$this->size = self::human_filesize(filesize($tmpname));
|
||||
|
@ -67,33 +67,84 @@ class Upload
|
|||
|
||||
$s3->putObject([
|
||||
'Bucket' => NGALLERY['root']['storage']['s3']['credentials']['bucket'],
|
||||
'Key' => $location.$filecdn,
|
||||
'Key' => $location . $filecdn,
|
||||
'SourceFile' => $tmpname
|
||||
]);
|
||||
$this->type = $type;
|
||||
$this->src = NGALLERY['root']['storage']['s3']['domains']['public'] . '/' . $location . $filecdn;
|
||||
$this->size = self::human_filesize(filesize($tmpname));
|
||||
$this->name = $name;
|
||||
}
|
||||
else
|
||||
{
|
||||
$location = "your-location";
|
||||
$folder = "{$location}/" . basename($tmpname);
|
||||
|
||||
$uploadDir = "{$_SERVER['DOCUMENT_ROOT']}/uploads/{$location}";
|
||||
} else {
|
||||
// Формирование путей
|
||||
$uploadDir = $_SERVER['DOCUMENT_ROOT'] . "/uploads{$location}";
|
||||
$destination = "{$uploadDir}/{$filecdn}";
|
||||
|
||||
// Создание директории
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
$destination = "{$uploadDir}/" . basename($tmpname);
|
||||
// Перемещение файла
|
||||
if (is_uploaded_file($tmpname)) {
|
||||
move_uploaded_file($tmpname, $destination);
|
||||
} else {
|
||||
rename($tmpname, $destination);
|
||||
}
|
||||
|
||||
// Установка свойств
|
||||
$this->type = $type;
|
||||
$this->src = "/uploads/{$folder}";
|
||||
$this->size = self::human_filesize(filesize($tmpname));
|
||||
$this->src = "/uploads/{$location}/{$filecdn}"; // Корректный URL
|
||||
$this->size = self::human_filesize(filesize($destination));
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
|
||||
public function generatePreview($width, $height)
|
||||
{
|
||||
if ($this->type !== 'image') return;
|
||||
|
||||
$src = $_SERVER['DOCUMENT_ROOT'] . $this->src;
|
||||
$image = null;
|
||||
|
||||
switch (mime_content_type($src)) {
|
||||
case 'image/jpeg':
|
||||
$image = imagecreatefromjpeg($src);
|
||||
break;
|
||||
case 'image/png':
|
||||
$image = imagecreatefrompng($src);
|
||||
break;
|
||||
case 'image/gif':
|
||||
$image = imagecreatefromgif($src);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
$originalWidth = imagesx($image);
|
||||
$originalHeight = imagesy($image);
|
||||
|
||||
$preview = imagecreatetruecolor($width, $height);
|
||||
imagecopyresampled(
|
||||
$preview,
|
||||
$image,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$width,
|
||||
$height,
|
||||
$originalWidth,
|
||||
$originalHeight
|
||||
);
|
||||
|
||||
$previewPath = $_SERVER['DOCUMENT_ROOT'] . '/cdn/previews/' . basename($this->src);
|
||||
imagejpeg($preview, $previewPath, 85);
|
||||
imagedestroy($preview);
|
||||
|
||||
$this->previewUrl = '/cdn/previews/' . basename($this->src);
|
||||
}
|
||||
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
|
@ -114,4 +165,3 @@ class Upload
|
|||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Services\GenerateRandomStr;
|
||||
|
@ -17,4 +18,29 @@ class Word
|
|||
}
|
||||
return $len;
|
||||
}
|
||||
}
|
||||
public static function processMentions($text) {
|
||||
return preg_replace_callback(
|
||||
'/@\[(\d++):([^\]\r\n]+)\]/u',
|
||||
function ($matches) {
|
||||
if (count($matches) !== 3) {
|
||||
return $matches[0] ?? '';
|
||||
}
|
||||
|
||||
$userId = (int)$matches[1];
|
||||
$username = trim($matches[2]);
|
||||
|
||||
// Экранируем только для HTML-атрибута, а не для видимой части
|
||||
$attrUsername = htmlspecialchars($username, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
return '<span class="user-mention" '
|
||||
. 'data-user-id="' . $userId . '" '
|
||||
. 'data-username="' . $attrUsername . '">'
|
||||
. '@' . $username
|
||||
. '</span>';
|
||||
},
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -22,7 +22,13 @@
|
|||
"chriskonnertz/bbcode": "^1.1",
|
||||
"paquettg/php-html-parser": "^2.2",
|
||||
"phpmailer/phpmailer": "^6.9",
|
||||
"beeyev/disposable-email-filter-php": "^1.3"
|
||||
"beeyev/disposable-email-filter-php": "^1.3",
|
||||
"symfony/console": "^7.2",
|
||||
"ratchet/pawl": "^0.4.3",
|
||||
"ratchetio/ratchetio": "^0.4.1",
|
||||
"cboden/ratchet": "0.4.4",
|
||||
"defuse/php-encryption": "^2.4",
|
||||
"symfony/process": "^7.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "*"
|
||||
|
|
1445
composer.lock
generated
11
index.php
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Prevent worker script termination when a client connection is interrupted
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
session_start();
|
||||
use App\Core\{Routes, Page};
|
||||
use App\Services\DB;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
@ -20,8 +20,15 @@ class App
|
|||
Debugger::enable();
|
||||
}
|
||||
try {
|
||||
|
||||
if (NGALLERY['root']['maintenance'] === false) {
|
||||
DB::connect();
|
||||
DB::init([
|
||||
'driver' => 'mysql',
|
||||
'host' => NGALLERY['root']['db']['host'],
|
||||
'database' => NGALLERY['root']['db']['name'],
|
||||
'username' => NGALLERY['root']['db']['login'],
|
||||
'password' => NGALLERY['root']['db']['password'],
|
||||
]);
|
||||
Routes::init();
|
||||
} else {
|
||||
Page::set('Errors/ServerDown');
|
||||
|
|
|
@ -8,6 +8,7 @@ ngallery:
|
|||
keywords: ""
|
||||
maintenance: false
|
||||
debug: true
|
||||
alloweddomains: ["nativegallery.loc", "pub-f05d2c8192d549e4b52535d646e5909a.r2.dev"]
|
||||
botkey: ''
|
||||
access:
|
||||
type: 'allow'
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
.cmt-submit { font-size:11px; color:#777; }
|
||||
.cmt-subscribe { padding:11px; }
|
||||
|
||||
#wtext { width:100%; height:150px; margin-bottom:10px; }
|
||||
#wtext { width:100%; height:150px; margin-bottom:10px; overflow: auto; word-break: break-word; }
|
||||
|
||||
.wvote { margin-left:5px; position:relative; z-index:10; }
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#photobar { margin:0 -20px; position:relative; background-color:#333; }
|
||||
#underphoto_frame { display:inline-block; }
|
||||
|
||||
#ph { max-width:50%; cursor:zoom-in; }
|
||||
#ph { max-width:30%; cursor:zoom-in; }
|
||||
#ph.v-zoom { max-height:calc(100vh - 30px); }
|
||||
#ph.zoomed { max-width:none; max-height:none; cursor:zoom-out; }
|
||||
#ph.nozoom { cursor:default; }
|
||||
|
|
|
@ -808,4 +808,111 @@ input.ml-button { height:22px; }
|
|||
.form-submit input[type="submit"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.tabs {
|
||||
font-family: Tahoma, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
padding: 4px 6px 0;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
background: linear-gradient(to bottom, #f3f3f3 0%, #e6e6e6 100%);
|
||||
border: 1px solid #7a7a7a;
|
||||
border-bottom: 0;
|
||||
border-radius: 3px 3px 0 0;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
color: #1a1a1a;
|
||||
position: relative;
|
||||
outline: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.tab-item:hover:not(.active):not(:disabled) {
|
||||
background: linear-gradient(to bottom, #ffffff 0%, #f0f0f0 100%);
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background: #ffffff;
|
||||
border-color: #181c20;
|
||||
color: #000000;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.tab-item:disabled {
|
||||
background: #eeeeee;
|
||||
color: #6d6d6d;
|
||||
cursor: not-allowed;
|
||||
border-color: #bdbdbd;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
border-top: 0;
|
||||
padding: 12px;
|
||||
background: #ffffff;
|
||||
min-height: 200px;
|
||||
}
|
||||
.styled-input {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.styled-input input {
|
||||
width: 100%;
|
||||
padding: 10px 10px 10px 0;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-bottom: 2px solid #ccc;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.styled-input input:focus {
|
||||
border-bottom: 2px solid #000;
|
||||
}
|
||||
|
||||
.styled-input label {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
pointer-events: none;
|
||||
transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
.styled-input input:focus~label,
|
||||
.styled-input input:valid~label {
|
||||
top: -20px;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
/* Для базовой анимации */
|
||||
.prw-animate {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Эффекты загрузки */
|
||||
.blur-load {
|
||||
filter: blur(10px);
|
||||
transition: filter 0.4s ease;
|
||||
}
|
||||
|
||||
.blur-load.loaded {
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
.hdshade {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
BIN
static/img/brokenimg.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
static/img/queueimg.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
static/img/smileys/1/acute.gif
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
static/img/smileys/1/aggressive.gif
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
static/img/smileys/1/agree.gif
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
static/img/smileys/1/aikido.gif
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
static/img/smileys/1/air_kiss.gif
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
static/img/smileys/1/alcoholic.gif
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
static/img/smileys/1/angel.gif
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/img/smileys/1/assassin.gif
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
static/img/smileys/1/bad.gif
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
static/img/smileys/1/banned.gif
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
static/img/smileys/1/beach.gif
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/img/smileys/1/beee.gif
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
static/img/smileys/1/beta.gif
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
static/img/smileys/1/big_boss.gif
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
static/img/smileys/1/black_eye.gif
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
static/img/smileys/1/blind.gif
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
static/img/smileys/1/blum2.gif
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
static/img/smileys/1/blum3.gif
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
static/img/smileys/1/blush2.gif
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
static/img/smileys/1/boast.gif
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
static/img/smileys/1/boredom.gif
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
static/img/smileys/1/brunette.gif
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
static/img/smileys/1/buba.gif
Normal file
After Width: | Height: | Size: 7 KiB |
BIN
static/img/smileys/1/buba_phone.gif
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
static/img/smileys/1/butcher.gif
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/img/smileys/1/censored.gif
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
static/img/smileys/1/clapping.gif
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
static/img/smileys/1/comando.gif
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/img/smileys/1/cray.gif
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
static/img/smileys/1/cray2.gif
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
static/img/smileys/1/crazy.gif
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
static/img/smileys/1/crazy_pilot.gif
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
static/img/smileys/1/curtsey.gif
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
static/img/smileys/1/dance.gif
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
static/img/smileys/1/dance2.gif
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
static/img/smileys/1/dance3.gif
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
static/img/smileys/1/dance4.gif
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
static/img/smileys/1/dash1.gif
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/img/smileys/1/dash2.gif
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
static/img/smileys/1/dash3.gif
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
static/img/smileys/1/declare.gif
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
static/img/smileys/1/ded_moroz.gif
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/img/smileys/1/ded_snegurochka.gif
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/img/smileys/1/ded_snegurochka2.gif
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/img/smileys/1/dinamo.gif
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
static/img/smileys/1/dirol.gif
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
static/img/smileys/1/dntknw.gif
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
static/img/smileys/1/don-t_mention.gif
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
static/img/smileys/1/download.gif
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
static/img/smileys/1/drinks.gif
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/img/smileys/1/dwarf.gif
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
static/img/smileys/1/elf.gif
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
static/img/smileys/1/facepalm.gif
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
static/img/smileys/1/fan_1.gif
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
static/img/smileys/1/fans.gif
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
static/img/smileys/1/feminist.gif
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
static/img/smileys/1/feminist_en.gif
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
static/img/smileys/1/first_move.gif
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
static/img/smileys/1/flirt.gif
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
static/img/smileys/1/focus.gif
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
static/img/smileys/1/fool.gif
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
static/img/smileys/1/friends.gif
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
static/img/smileys/1/gamer1.gif
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
static/img/smileys/1/gamer2.gif
Normal file
After Width: | Height: | Size: 3.8 KiB |