diff --git a/app/Controllers/Api/Admin/Images/SetVisibility.php b/app/Controllers/Api/Admin/Images/SetVisibility.php index 4ef1e16..4e5acf2 100644 --- a/app/Controllers/Api/Admin/Images/SetVisibility.php +++ b/app/Controllers/Api/Admin/Images/SetVisibility.php @@ -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 { diff --git a/app/Controllers/Api/Emoji/Load.php b/app/Controllers/Api/Emoji/Load.php new file mode 100644 index 0000000..05db9f2 --- /dev/null +++ b/app/Controllers/Api/Emoji/Load.php @@ -0,0 +1,35 @@ + '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' + ]); + } + } +} diff --git a/app/Controllers/Api/Images/Comments/Create.php b/app/Controllers/Api/Images/Comments/Create.php index 89bebb2..16cb8ad 100644 --- a/app/Controllers/Api/Images/Comments/Create.php +++ b/app/Controllers/Api/Images/Comments/Create.php @@ -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', diff --git a/app/Controllers/Api/Images/Compress.php b/app/Controllers/Api/Images/Compress.php index 34f4ce0..3f4d1f5 100644 --- a/app/Controllers/Api/Images/Compress.php +++ b/app/Controllers/Api/Images/Compress.php @@ -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; } } -?> diff --git a/app/Controllers/Api/Images/LoadMap.php b/app/Controllers/Api/Images/LoadMap.php new file mode 100644 index 0000000..5c3f3e1 --- /dev/null +++ b/app/Controllers/Api/Images/LoadMap.php @@ -0,0 +1,110 @@ +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; + } +} diff --git a/app/Controllers/Api/Images/LoadRecent.php b/app/Controllers/Api/Images/LoadRecent.php index 1c05f03..4a682bc 100644 --- a/app/Controllers/Api/Images/LoadRecent.php +++ b/app/Controllers/Api/Images/LoadRecent.php @@ -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); + } + +} \ No newline at end of file diff --git a/app/Controllers/Api/Images/Upload.php b/app/Controllers/Api/Images/Upload.php index 96def40..093245c 100644 --- a/app/Controllers/Api/Images/Upload.php +++ b/app/Controllers/Api/Images/Upload.php @@ -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( diff --git a/app/Controllers/Api/Register.php b/app/Controllers/Api/Register.php index 12144b0..97b1a1e 100644 --- a/app/Controllers/Api/Register.php +++ b/app/Controllers/Api/Register.php @@ -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); diff --git a/app/Controllers/Api/Users/Search.php b/app/Controllers/Api/Users/Search.php new file mode 100644 index 0000000..0fe7298 --- /dev/null +++ b/app/Controllers/Api/Users/Search.php @@ -0,0 +1,26 @@ + '%' . $query . '%')); + foreach ($users as $u) { + $result[] = [ + 'id' => $u['id'], + 'username' => $u['username'], + 'photourl' => $u['photourl'], + ]; + } + echo json_encode($result); + } +} diff --git a/app/Controllers/ApiController.php b/app/Controllers/ApiController.php index fa137d9..fe64043 100644 --- a/app/Controllers/ApiController.php +++ b/app/Controllers/ApiController.php @@ -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(); + } } \ No newline at end of file