mirror of
https://github.com/claradex/nativegallery.git
synced 2025-06-03 13:03:54 +03:00
update api
This commit is contained in:
parent
a0671bfba8
commit
d701885d2b
10 changed files with 741 additions and 135 deletions
|
@ -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
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
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(
|
||||
|
|
|
@ -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
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();
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in a new issue