<?php

/*
 * This file is part of Chevereto.
 *
 * (c) Rodolfo Berrios <rodolfo@chevereto.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Chevere\ThrowableHandler\Documents\PlainDocument;
use Chevereto\Config\Config;
use Chevereto\Legacy\Classes\Akismet;
use Chevereto\Legacy\Classes\Album;
use Chevereto\Legacy\Classes\ApiKey;
use Chevereto\Legacy\Classes\Categories;
use Chevereto\Legacy\Classes\Category;
use Chevereto\Legacy\Classes\DB;
use Chevereto\Legacy\Classes\Follow;
use Chevereto\Legacy\Classes\HybridauthSession;
use Chevereto\Legacy\Classes\Image;
use Chevereto\Legacy\Classes\Import;
use Chevereto\Legacy\Classes\IpBan;
use Chevereto\Legacy\Classes\Like;
use Chevereto\Legacy\Classes\Listing;
use Chevereto\Legacy\Classes\Login;
use Chevereto\Legacy\Classes\Notification;
use Chevereto\Legacy\Classes\Search;
use Chevereto\Legacy\Classes\Settings;
use Chevereto\Legacy\Classes\Stat;
use Chevereto\Legacy\Classes\Storage;
use Chevereto\Legacy\Classes\Tag;
use Chevereto\Legacy\Classes\TwoFactor;
use Chevereto\Legacy\Classes\Upload;
use Chevereto\Legacy\Classes\User;
use Chevereto\Legacy\G\Handler;
use Hybridauth\Hybridauth;
use function Chevere\Message\message;
use function Chevere\Standard\randomString;
use function Chevere\ThrowableHandler\throwableHandler;
use function Chevere\Writer\writers;
use function Chevere\xrDebug\PHP\throwableHandler as XrThrowableHandler;
use function Chevereto\Legacy\assertMaxCount;
use function Chevereto\Legacy\decodeID;
use function Chevereto\Legacy\encodeID;
use function Chevereto\Legacy\G\array_filter_array;
use function Chevereto\Legacy\G\check_value;
use function Chevereto\Legacy\G\datetime;
use function Chevereto\Legacy\G\datetimegmt;
use function Chevereto\Legacy\G\fetch_url;
use function Chevereto\Legacy\G\get_base_url;
use function Chevereto\Legacy\G\get_bytes;
use function Chevereto\Legacy\G\get_client_ip;
use function Chevereto\Legacy\G\get_current_url;
use function Chevereto\Legacy\G\get_public_url;
use function Chevereto\Legacy\G\json_document_output;
use function Chevereto\Legacy\G\nullify_string;
use function Chevereto\Legacy\G\require_theme_file;
use function Chevereto\Legacy\G\starts_with;
use function Chevereto\Legacy\getSetting;
use function Chevereto\Legacy\getVariable;
use function Chevereto\Legacy\isDebug;
use function Chevereto\Legacy\isShowEmbedContent;
use function Chevereto\Legacy\send_mail;
use function Chevereto\Legacy\time_elapsed_string;
use function Chevereto\Vars\env;
use function Chevereto\Vars\files;
use function Chevereto\Vars\post;
use function Chevereto\Vars\request;
use function Chevereto\Vars\requestHeaders;
use function Chevereto\Vars\session;

return function (Handler $handler) {
    try {
        $POST = post();
        $REQUEST = request();
        $HEADERS = requestHeaders();
        $REQUEST['auth_token'] ??= $HEADERS['X-Auth-Token'] ?? '';
        $REQUEST['action'] ??= $HEADERS['X-Action'] ?? '';
        if (! $handler::checkAuthToken($REQUEST['auth_token'] ?? '')) {
            throw new Exception(_s('Request denied'), 401);
        }
        $logged_user = Login::getUser();
        $logged_user_source_db = [
            'user_name' => $logged_user['name'] ?? null,
            'user_username' => $logged_user['username'] ?? null,
            'user_email' => $logged_user['email'] ?? null,
        ];
        $doing = $REQUEST['action'];
        if ($logged_user && $logged_user['status'] !== 'valid') {
            $doing = 'deny';
        }
        if (in_array($doing, ['importStats', 'importEdit', 'importDelete', 'importReset', 'importResume'], true)) {
            if (Login::isAdmin() === false) {
                throw new Exception(_s('Request denied'), 403);
            }
            $import = new Import();
        }
        if (in_array($doing, ['chunked-upload', 'upload-chunk', 'upload'], true)) {
            if (! $handler::cond('upload_allowed')) {
                throw new Exception(_s('Request denied'), 403);
            }
            $REQUEST['type'] ??= $HEADERS['X-Type'] ?? '';
            if ($doing !== 'upload-chunk') {
                $source = $REQUEST['type'] === 'file'
                    ? files()['source']
                    : $REQUEST['source'];
            }
            /** @var ?int $ownerId */
            $ownerId = $logged_user['id'] ?? null;
            $REQUEST['owner'] ??= $HEADERS['X-Owner'] ?? null;
            if ((Login::isAdmin() || Login::isManager()) && ! empty($REQUEST['owner'])) {
                $ownerId = decodeID($REQUEST['owner']);
            }
        }
        $chunkUploadSize = getSetting('chunk_upload_size');
        switch ($doing) {
            case 'chunked-upload':
                $maxSize = get_bytes(getSetting('upload_max_filesize_mb') . ' MB');
                $checksum = $REQUEST['checksum'] ?? '';
                $size = (int) ($REQUEST['size'] ?? 0);
                if (! preg_match('/^[a-f0-9]{16,}$/', $checksum)) {
                    throw new Exception('Invalid file checksum' . $checksum, 100);
                }
                if ($size === 0) {
                    throw new Exception('Invalid file size', 100);
                }
                if ($source === '') {
                    throw new Exception('Invalid file name', 100);
                }
                $extension = strtolower(pathinfo($source, PATHINFO_EXTENSION));
                if ($extension === '') {
                    throw new Exception('Missing file extension', 100);
                }
                if (! in_array($extension, Image::getEnabledImageExtensions(), true)) {
                    throw new Exception('Unsupported file extension', 100);
                }
                if ($size > $maxSize) {
                    throw new Exception('File size exceeds maximum', 101);
                }
                $do_dupe_check = ! getSetting('enable_duplicate_uploads') && ! Login::isAdmin();
                if ($do_dupe_check && (Image::isDuplicatedChunkUpload($checksum) || Image::isDuplicatedUpload($checksum))) {
                    throw new Exception(_s('Duplicated upload'), 101);
                }
                $token = randomString(64);
                $uploadId = DB::insert('uploads', [
                    'user_id' => $ownerId,
                    'uploader_ip' => get_client_ip(),
                    'token' => $token,
                    'checksum' => $checksum,
                    'params' => json_encode([
                        'source' => $REQUEST['source'],
                    ]),
                    'chunks' => ceil($size / $chunkUploadSize),
                ]);
                $json_array['status_code'] = 200;
                $json_array['success'] = [
                    'message' => 'chunked upload',
                    'code' => 200,
                    'upload_id' => encodeID($uploadId),
                    'token' => $token,
                    'hash' => hash_hmac(
                        'sha256',
                        $uploadId . $token,
                        getVariable('hmac_secret_upload')->string()
                    ),
                ];

                break;
            case 'upload-chunk':
                if ($logged_user !== []) {
                    session_write_close();
                }
                $uploadId = decodeID($HEADERS['X-Upload-Id']);
                $index = (int) ($HEADERS['X-Index'] ?? 0);
                $token = $HEADERS['X-Token'] ?? '';
                $hash = $HEADERS['X-Hash'] ?? '';
                if ($index === 0) {
                    throw new Exception('Invalid chunk index', 100);
                }
                if ($token === '') {
                    throw new Exception('Invalid token', 100);
                }
                if ($hash === '') {
                    throw new Exception('Invalid hash', 100);
                }
                $calcHash = hash_hmac(
                    'sha256',
                    $uploadId . $token,
                    getVariable('hmac_secret_upload')->string()
                );
                if (! hash_equals($calcHash, $hash)) {
                    throw new Exception('Invalid hash', 100);
                }
                $uploadWhere = [
                    'id' => $uploadId,
                    'token' => $token,
                ];
                if ($logged_user !== []) {
                    $uploadWhere['user_id'] = $logged_user['id'];
                }
                $uploadRow = DB::get(
                    table: 'uploads',
                    where: $uploadWhere,
                    limit: 1,
                );
                if (! $uploadRow) {
                    throw new Exception('Missing upload id', 100);
                }
                if ($index > $uploadRow['upload_chunks']) {
                    throw new Exception('Invalid chunk index', 100);
                }
                $db = DB::getInstance();
                $db->query(
                    'SELECT COUNT(*) c FROM '
                    . DB::getTable('uploads_chunks')
                    . ' WHERE upload_chunk_upload_id=:upload_id AND upload_chunk_index=:chunk_index;'
                );
                $db->bind(':upload_id', $uploadId);
                $db->bind(':chunk_index', $index);
                if ($db->fetchSingle()['c'] > 0) {
                    throw new Exception('Chunk already uploaded', 100);
                }
                // $chunkFile = $source['tmp_name'];
                // if (! file_exists($chunkFile)) {
                //     throw new Exception('Missing chunk file', 100);
                // }
                // $chunkFilesize = filesize($chunkFile);
                // if ($chunkFilesize === 0) {
                //     throw new Exception('Empty chunk file', 100);
                // }
                // if ($chunkFilesize > $chunkUploadSize) {
                //     throw new Exception('Chunk file size exceeds maximum', 101);
                // }
                // Handle chunk upload as a stream (for "source" stream input)
                $chunkFile = Upload::getTempNam(suffix: "{$uploadId}_{$index}");
                $inputStream = fopen('php://input', 'rb');
                if ($inputStream === false) {
                    throw new Exception('Failed to open input stream', 100);
                }
                $outputStream = fopen($chunkFile, 'wb');
                if ($outputStream === false) {
                    fclose($inputStream);

                    throw new Exception('Failed to open chunk file for writing', 100);
                }
                stream_copy_to_stream($inputStream, $outputStream);
                fclose($inputStream);
                fclose($outputStream);
                if (! file_exists($chunkFile) || filesize($chunkFile) === 0) {
                    throw new Exception('Failed to write chunk file', 100);
                }
                DB::insert('uploads_chunks', [
                    'upload_id' => $uploadId,
                    'index' => $index,
                    'path' => $chunkFile,
                ]);
                $json_array['status_code'] = 200;
                $json_array['success'] = [
                    'message' => 'chunk uploaded',
                    'code' => 200,
                ];

                break;
            case 'upload': // EX 100
                // NOTE: This is considering assets and user uploads as the same "upload" action
                $type = $REQUEST['type'];
                if (isset($REQUEST['what'])
                    && in_array($REQUEST['what'], ['avatar', 'background'], true)
                ) {
                    if ($logged_user === []) {
                        throw new Exception(_s('Login needed'), 403);
                    }
                    if (! $handler::cond('content_manager') && $ownerId !== $logged_user['id']) {
                        throw new Exception('Invalid content owner request', 115);
                    }
                    $user_picture_upload = User::uploadPicture(
                        $ownerId === $logged_user['id']
                            ? $logged_user
                            : $ownerId,
                        $REQUEST['what'],
                        $source
                    );
                    $json_array['success'] = [
                        'image' => $user_picture_upload,
                        'message' => sprintf('%s picture uploaded', ucfirst($type)),
                        'code' => 200,
                    ];

                    break;
                }
                if ($handler::cond('forced_private_mode')) {
                    $REQUEST['privacy'] = getSetting('website_content_privacy_mode');
                }
                if (! empty($REQUEST['album_id'])) {
                    $REQUEST['album_id'] = decodeID($REQUEST['album_id']);
                }
                // TODO: Unify this check
                if (! $handler::cond('content_manager') && getSetting('akismet')) {
                    Akismet::checkImage(
                        title: $REQUEST['title'] ?? null,
                        description: $REQUEST['description'] ?? null,
                        tags: $REQUEST['tags'] ?? null,
                        source_db: $logged_user_source_db
                    );
                }
                $uploadToWebsite = Image::uploadToWebsite($source, $logged_user, $REQUEST);
                if ($logged_user !== []) {
                    session_write_close();
                }
                $uploaded_id = intval($uploadToWebsite[0]);
                $json_array['status_code'] = 200;
                $json_array['success'] = [
                    'message' => 'file uploaded',
                    'code' => 200,
                ];
                $image = Image::getSingle($uploaded_id);
                if ($image === []) {
                    throw new LogicException(
                        message('Missing image')
                    );
                }
                $image = Image::formatArray($image, true);
                $image['delete_url'] = Image::getDeleteUrl(
                    type: $image['type'],
                    idEncoded: encodeID($uploaded_id),
                    password: $uploadToWebsite[1]
                );
                if (! $image['is_approved']) {
                    unset($image['image']['url'], $image['thumb']['url'], $image['medium']['url'], $image['url'], $image['display_url']);
                }
                $json_array['image'] = $image;

                break;
            case 'get-album-contents':
            case 'list': // EX 200
                if ($doing === 'get-album-contents') {
                    if (! isShowEmbedContent()) {
                        throw new Exception(_s('Request denied'), 403);
                    }
                    $list_request = 'images';
                    $aux = $REQUEST['albumid'];
                    $REQUEST = null;
                    $REQUEST['albumid'] = $aux;
                } else {
                    $list_request = $REQUEST['list'];
                }
                if (! in_array($list_request, ['images', 'albums', 'users', 'tags'], true)) {
                    throw new Exception('Invalid list request', 100);
                }
                $output_tpl = $list_request;
                if (isset($REQUEST['params_hidden']) && is_array($REQUEST['params_hidden'])) {
                    $params_hidden = [];
                    foreach ($REQUEST['params_hidden'] as $k => $v) {
                        if (isset($REQUEST[$k])) {
                            $params_hidden[$k] = $v;
                        }
                    }
                }
                if (! empty($REQUEST['albumid'])) {
                    $album_id = decodeID($REQUEST['albumid']);
                }
                $ownerId = null;
                $where = '';
                switch ($list_request) {
                    case 'images':
                        $binds = [];
                        $where = '';
                        if (! empty($REQUEST['like_user_id'])) {
                            $where .= 'WHERE like_user_id=:image_user_id';
                            $binds[] = [
                                'param' => ':image_user_id',
                                'value' => decodeID($REQUEST['like_user_id']),
                            ];
                        }
                        if (! empty($REQUEST['follow_user_id'])) {
                            $where .= ($where === '' ? 'WHERE' : ' AND') . ' follow_user_id=:image_user_id';
                            $binds[] = [
                                'param' => ':image_user_id',
                                'value' => decodeID($REQUEST['follow_user_id']),
                            ];
                        }
                        if (! empty($REQUEST['userid'])) {
                            $ownerId = decodeID($REQUEST['userid']);
                            $where .= ($where === '' ? 'WHERE' : ' AND') . ' image_user_id=:image_user_id';
                            $binds[] = [
                                'param' => ':image_user_id',
                                'value' => $ownerId,
                            ];
                        }
                        if (isset($album_id)) {
                            $where .= ($where === '' ? 'WHERE' : ' AND') . ' image_album_id=:image_album_id';
                            $binds[] = [
                                'param' => ':image_album_id',
                                'value' => $album_id,
                            ];
                            $album = Album::getSingle($album_id);
                            if ($album['user']['id'] ?? false) {
                                $ownerId = $album['user']['id'];
                            }
                            if ($album['privacy'] === 'password'
                                && (
                                    ! $handler::cond('content_manager')
                                    && $ownerId !== ($logged_user['id'] ?? 0)
                                    && ! Album::checkSessionPassword($album)
                                )
                            ) {
                                throw new Exception(_s('Request denied'), 403);
                            }
                        }
                        if (! empty($REQUEST['category_id']) && is_numeric($REQUEST['category_id'])) {
                            $category = $REQUEST['category_id'];
                        }
                        if (isset($REQUEST['from'])) {
                            switch ($REQUEST['from']) {
                                case 'user':
                                    $output_tpl = 'user/images';

                                    break;
                                case 'album':
                                    $output_tpl = 'album/images';

                                    break;
                            }
                        }

                        break;
                    case 'albums':
                        $binds = [];
                        $where = '';
                        if (! empty($REQUEST['userid'])) {
                            $ownerId = decodeID($REQUEST['userid']);
                            $where .= 'WHERE album_user_id=:album_user_id';
                            $binds[] = [
                                'param' => ':album_user_id',
                                'value' => $ownerId,
                            ];
                        }
                        if (isset($REQUEST['from'])) {
                            switch ($REQUEST['from']) {
                                case 'user':
                                    $output_tpl = 'user/albums';

                                    break;
                                case 'album':
                                    $output_tpl = 'album';

                                    break;
                            }
                        }
                        if (isset($album_id)) {
                            $where .= ($where === '' ? 'WHERE' : ' AND') . ' album_parent_id=:album_id';
                            $binds[] = [
                                'param' => ':album_id',
                                'value' => $album_id,
                            ];
                        }

                        break;
                    case 'users':
                        $where = '';
                        if (getSetting('enable_followers')
                            && (! empty($REQUEST['following_user_id']) || ! empty($REQUEST['followers_user_id']))
                        ) {
                            $doing = ! empty($REQUEST['following_user_id'])
                                ? 'following'
                                : 'followers';
                            $user_id = decodeID(
                                $doing === 'following'
                                    ? $REQUEST['following_user_id']
                                    : $REQUEST['followers_user_id']
                            );
                            $where = 'WHERE follow'
                                . (
                                    $doing === 'following'
                                        ? ''
                                        : '_followed'
                                )
                                . '_user_id=:user_id';
                            $binds[] = [
                                'param' => ':user_id',
                                'value' => $user_id,
                            ];
                        }

                        break;
                }
                if (! empty($REQUEST['q'])) {
                    $search = new Search();
                    $search->q = $REQUEST['q'];
                    $search->type = $list_request;
                    $search->request = $REQUEST;
                    $search->requester = Login::getUser();
                    $search->build();
                    if (! check_value($search->q)) {
                        throw new Exception('Missing search term', 400);
                    }
                    $where .= $where === '' ? $search->wheres : preg_replace('/WHERE /', ' AND ', $search->wheres, 1);
                    $binds = array_merge($binds ?? [], $search->binds);
                }
                $getParams = Listing::getParams(request(), true);
                if ($getParams['sort'][0] === 'likes' && ! getSetting('enable_likes')) {
                    throw new Exception(_s('Request denied'), 403);
                }
                $album_fetch = 0;
                if ($doing === 'get-album-contents' && isset($album['image_count'])) {
                    $album_fetch = min(1000, $album['image_count']);
                    $getParams = [
                        'items_per_page' => $album_fetch,
                        'page' => 0,
                        'limit' => $album_fetch,
                        'offset' => 0,
                        'sort' => ['date', 'desc'],
                    ];
                }
                $listing = new Listing();
                if (array_key_exists('approved', $REQUEST)) {
                    if (Login::isAdmin() || $logged_user['is_manager']) {
                        $listing->setApproved((int) $REQUEST['approved']);
                    } else {
                        throw new Exception(_s('Request denied'), 403);
                    }
                }
                $listing->setType($list_request);
                if (isset($getParams['reverse'])) {
                    $listing->setReverse($getParams['reverse']);
                }
                if (isset($getParams['seek'])) {
                    $listing->setSeek($getParams['seek']);
                }
                $listing->setOffset($getParams['offset']);
                $listing->setLimit($getParams['limit']);
                $listing->setSortType($getParams['sort'][0]);
                $listing->setSortOrder($getParams['sort'][1]);
                if (isset($category)) {
                    $listing->setCategory($category);
                }
                $home_uids = getSetting('homepage_uids');
                if (Settings::get('homepage_style') === 'split'
                    && isset($home_uids)
                    && isset($POST['params_hidden']['route']) && $POST['params_hidden']['route'] === 'index'
                ) {
                    $home_uid_is_null = $home_uids === '' || $home_uids === '0';
                    $home_uid_arr = ! $home_uid_is_null
                        ? explode(',', $home_uids)
                        : false;
                    if ($home_uid_arr) {
                        $home_uid_bind = [];
                        foreach ($home_uid_arr as $k => $v) {
                            $home_uid_bind[] = ':user_id_' . $k;
                            if ($v === '') {
                                $home_uid_is_null = true;
                            }
                        }
                        $home_uid_bind = implode(',', $home_uid_bind);
                        $prefix = DB::getFieldPrefix($list_request);
                        $where = 'WHERE ' . $prefix . '_user_id IN(' . $home_uid_bind . ')';
                        if ($home_uid_is_null) {
                            $where .= ' OR '
                                . $prefix
                                . '_user_id IS NULL';
                        }
                        foreach ($home_uid_arr as $k => $v) {
                            $listing->bind(':user_id_' . $k, $v);
                        }
                    }
                }
                $listing->setWhere($where);
                if (isset($ownerId)) {
                    $listing->setOwner((int) $ownerId);
                }
                $listing->setRequester($logged_user);
                if (in_array($list_request, ['images', 'albums'], true)
                    && (
                        $handler::cond('content_manager')
                        || ($logged_user !== [] && $ownerId === $logged_user['id'])
                    )
                ) {
                    $listing->setTools(true);
                }
                if (! empty($params_hidden)) {
                    $listing->setParamsHidden($params_hidden);
                }
                if ($list_request === 'images' && ! empty($REQUEST['albumid'])) {
                    if ($handler::cond('forced_private_mode')) {
                        $album['privacy'] = getSetting('website_content_privacy_mode');
                    }
                    if (isset($album['privacy'])) {
                        $listing->setPrivacy($album['privacy']);
                    }
                }
                if (isset($binds)) {
                    foreach ($binds as $bind) {
                        $listing->bind($bind['param'], $bind['value']);
                    }
                }
                $listing->setOutputAssoc(true);
                $listing->exec();
                $json_array['status_code'] = 200;
                if ($doing === 'get-album-contents'
                    && isset($album, $album['image_count'])) {
                    $json_array['album'] = array_filter_array($album, ['id', 'creation_ip', 'password', 'user', 'privacy_extra', 'privacy_notes'], 'rest');
                    $contents = [];
                    foreach ($listing->outputAssoc() as $v) {
                        $contents[] = array_filter_array($v, ['title', 'id_encoded', 'url', 'url_short', 'path_viewer', 'url_viewer', 'filename', 'medium', 'thumb', 'type', 'url_frame'], 'exclusion');
                    }
                    $json_array['is_output_truncated'] = $album['image_count'] > $album_fetch ? 1 : 0;
                    $json_array['contents'] = $contents;
                } else {
                    $json_array['html'] = $listing->htmlOutput($output_tpl);
                }
                $json_array['seekEnd'] = $listing->seekEnd;

                break;
            case 'edit': // EX 3X
                if ($logged_user === []) {
                    throw new Exception(_s('Login needed'), 403);
                }
                $editing_request = $REQUEST['editing'];
                $editing = $editing_request;
                $type = $REQUEST['edit'];
                $ownerId = ! empty($REQUEST['owner']) ? decodeID($REQUEST['owner']) : $logged_user['id'];
                if (! in_array($type, ['image', 'album', 'images', 'albums', 'category', 'tag', 'storage', 'ip_ban'], true)) {
                    throw new Exception('Invalid edit request', 100);
                }
                if ($editing['id'] == null) {
                    throw new Exception('Missing edit target id', 100);
                }
                $id = decodeID($editing['id']);

                $editing['new_album'] = isset($editing['new_album'])
                    && $editing['new_album'] == 'true';
                $allowed_to_edit = [
                    'image' => ['category_id', 'title', 'tags', 'description', 'album_id', 'nsfw'],
                    'album' => ['name', 'privacy', 'album_id', 'description', 'password'],
                    'category' => ['name', 'description', 'url_key'],
                    'tag' => ['name', 'description'],
                    'storage' => [
                        'name',
                        'bucket',
                        'region',
                        'url',
                        'server',
                        'capacity',
                        'is_https',
                        'is_active',
                        'api_id',
                        'key',
                        'secret',
                        'account_id',
                        'account_name',
                        'type_chain',
                        'use_path_style_endpoint',
                    ],
                    'ip_ban' => ['ip', 'expires', 'message'],
                ];
                if (Handler::cond('content_manager')) {
                    array_push($allowed_to_edit['album'], 'cta_enable', 'cta');
                }
                $allowed_to_edit['images'] = $allowed_to_edit['image'];
                $allowed_to_edit['albums'] = $allowed_to_edit['album'];
                if ($editing['new_album']) {
                    $new_album = ['new_album', 'album_name', 'album_privacy', 'album_password', 'album_description'];
                    $allowed_to_edit['image'] = array_merge($allowed_to_edit['image'], $new_album);
                    $allowed_to_edit['album'] = array_merge($allowed_to_edit['album'], $new_album);
                }
                $editing = array_filter_array($editing, $allowed_to_edit[$type], 'exclusion');
                if ($handler::cond('forced_private_mode')
                    && in_array($type, ['album', 'image'], true)
                ) {
                    $editing[$type === 'album' ? 'privacy' : 'album_privacy'] = getSetting('website_content_privacy_mode');
                }
                if (isset($editing['album_id']) && $editing['album_id'] !== '') {
                    $editing['album_id'] = decodeID($editing['album_id']);
                    if ($editing['album_id'] === 0) {
                        unset($editing['album_id']);
                    }
                }
                if (count($editing) === 0) {
                    throw new Exception('Invalid edit request', 403);
                }
                switch ($type) {
                    case 'image':
                        $source_image_db = Image::getSingle($id);
                        if ($source_image_db === []) {
                            throw new Exception(_s("%s doesn't exists", _n('Image', 'Images', 1)), 100);
                        }
                        if (
                            isset($editing['nsfw'])
                            && $editing['nsfw'] != $source_image_db['image_nsfw']
                            && getSetting('image_lock_nsfw_editing')
                            && ! (Login::isAdmin() || $logged_user['is_manager'])
                        ) {
                            throw new Exception('Invalid request', 403);
                        }
                        if (! $handler::cond('content_manager')
                            && $source_image_db['image_user_id'] !== $logged_user['id']
                        ) {
                            throw new Exception('Invalid content owner request', 101);
                        }
                        if (isset($editing['new_album'])) {
                            if (! $handler::cond('content_manager') && getSetting('akismet')) {
                                Akismet::checkAlbum($editing['album_name'], $editing['album_description'], $source_image_db);
                            }
                            $inserted_album = Album::insert([
                                'name' => $editing['album_name'] ?? null,
                                'user_id' => $source_image_db['image_user_id'] ?? null,
                                'privacy' => $editing['album_privacy'] ?? null,
                                'description' => $editing['album_description'] ?? null,
                                'password' => $editing['album_password'] ?? null,
                            ]);
                            $editing['album_id'] = $inserted_album;
                        }
                        if (! empty($editing['category_id'])
                            && ! array_key_exists($editing['category_id'], $handler::var('categories'))
                        ) {
                            throw new Exception('Invalid category', 102);
                        }
                        unset($editing['album_privacy'], $editing['new_album'], $editing['album_name']);
                        if (! $handler::cond('content_manager')
                            && getSetting('akismet')
                        ) {
                            Akismet::checkImage(
                                title: $editing['title'] ?? null,
                                description: $editing['description'] ?? null,
                                tags: $editing['tags'] ?? null,
                                source_db: $source_image_db
                            );
                        }
                        Image::update($id, $editing);
                        $image_edit_db = Image::getSingle($id);
                        if ($image_edit_db === []) {
                            throw new LogicException(
                                message('Missing image')
                            );
                        }
                        if ($source_image_db['image_album_id'] !== $image_edit_db['image_album_id'] && $image_edit_db['image_album_id']) {
                            global $image_album_slice, $image_id;
                            $image_album_slice = Image::getAlbumSlice($id, (int) $image_edit_db['image_album_id'], 2);
                            $image_id = $image_edit_db['image_id'];
                        }
                        $album_id = $image_edit_db['image_album_id'];
                        $json_array['status_code'] = 200;
                        $json_array['success'] = [
                            'message' => _s('%s edited', _n('Image', 'Images', 1)),
                            'code' => 200,
                        ];
                        $json_array['editing'] = $editing_request;
                        $json_array['image'] = Image::formatArray($image_edit_db, true);
                        if (isset($image_album_slice)) {
                            // Add the album URL to the slice
                            $image_album_slice['url'] = Album::getUrl(encodeID((int) $album_id));
                            ob_start();
                            require_theme_file('snippets/image_album_slice');
                            $html = ob_get_contents();
                            ob_end_clean();
                            $json_array['image']['album']['slice'] = [
                                'next' => $image_album_slice['next']['path_viewer'] ?? '',
                                'prev' => $image_album_slice['prev']['path_viewer'] ?? '',
                                'html' => $html,
                            ];
                        } else {
                            $json_array['image']['album']['slice'] = null;
                        }

                        break;
                    case 'album':
                        $source_album_db = Album::getSingle(
                            id: $id,
                            pretty: false
                        );
                        if ($source_album_db === []) {
                            throw new Exception(_s("%s doesn't exists", _n('Album', 'Albums', 1)), 100);
                        }
                        if (! $handler::cond('content_manager') && $source_album_db['album_user_id'] !== $logged_user['id']) {
                            throw new Exception('Invalid content owner request', 102);
                        }
                        if (isset($editing['album_id']) || isset($editing['new_album'])) {
                            $album_move = true;
                            if (isset($editing['new_album'])) {
                                if (! $handler::cond('content_manager') && getSetting('akismet')) {
                                    Akismet::checkAlbum($editing['album_name'], $editing['album_description'], $source_album_db);
                                }
                                $editing['album_id'] = Album::insert([
                                    'name' => $editing['album_name'],
                                    'user_id' => $source_album_db['album_user_id'],
                                    'privacy' => $editing['album_privacy'],
                                    'description' => $editing['album_description'],
                                    'password' => $editing['album_password'],
                                ]);
                            } else {
                                if ($editing['album_id'] === '') {
                                    $editing['album_id'] = null;
                                }
                            }
                            // Note: Includes move album to album
                            Album::moveContents($id, $editing['album_id']);
                        } else {
                            unset($editing['album_privacy'], $editing['new_album'], $editing['album_name']);
                            if (! $handler::cond('content_manager') && getSetting('akismet')) {
                                Akismet::checkAlbum($editing['name'], $editing['description'], $source_album_db);
                            }
                            Album::update($id, $editing);
                        }
                        $album_edited = Album::getSingle((int) ($editing['album_id'] ?? $id));
                        if ($album_edited === []) {
                            throw new Exception("Edited album doesn't exists", 100);
                        }
                        $json_array['status_code'] = 200;
                        $json_array['success'] = [
                            'message' => _s('%s edited', _s('Content')),
                            'code' => 200,
                        ];
                        $json_array['album'] = $album_edited;
                        if (isset($album_move)) {
                            $json_array['old_album'] = Album::formatArray(
                                Album::getSingle(id: $id, pretty: false),
                                true
                            );
                            $json_array['album']['html'] = Listing::getAlbumHtml($album_edited['id'] ?? '');
                            $json_array['old_album']['html'] = Listing::getAlbumHtml($id);
                        }

                        break;
                    case 'category':
                        if (! Login::isAdmin()) {
                            throw new Exception('Invalid content owner request', 107);
                        }
                        $id = $REQUEST['editing']['id'];
                        if (! array_key_exists($id, $handler::var('categories'))) {
                            throw new Exception('Invalid target category', 100);
                        }
                        if (! isset($editing['name'])) {
                            throw new Exception('Invalid name', 101);
                        }
                        if (! preg_match('/^[\-\w]+$/', $editing['url_key'] ?? '')) {
                            throw new Exception('Invalid category URL key', 102);
                        }
                        if (is_array($handler::var('categories'))) {
                            foreach ($handler::var('categories') as $v) {
                                if ($v['id'] === intval($id)) {
                                    continue;
                                }
                                if ($v['url_key'] === $editing['url_key']) {
                                    $category_error = true;

                                    break;
                                }
                            }
                        }
                        if ($category_error ?? false) {
                            throw new Exception(_s('%s URL key already being used.', _s('Category')), 103);
                        }
                        nullify_string($editing['description']);
                        $update_category = DB::update('categories', $editing, [
                            'id' => $id,
                        ]);
                        if (! $update_category) {
                            throw new Exception('Failed to edit', 400);
                        }
                        $category = DB::get('categories', [
                            'id' => $id,
                        ])[0];
                        $category['category_url'] = get_base_url('category/' . $category['category_url_key']);
                        $category = DB::formatRow($category);
                        $json_array['status_code'] = 200;
                        $json_array['success'] = [
                            'message' => _s('%s edited', _s('Category')),
                            'code' => 200,
                        ];
                        $json_array['category'] = $category;
                        Categories::deleteCache();

                        break;
                    case 'tag':
                        if (! $handler::cond('content_manager')) {
                            throw new Exception('Invalid content manager request', 107);
                        }
                        $id = $REQUEST['editing']['id'];
                        if (! isset($editing['name'])) {
                            throw new Exception('Invalid name', 101);
                        }
                        nullify_string($editing['description']);
                        $update_tag = Tag::update($id, $editing);
                        if ($update_tag === false) {
                            throw new Exception('Failed to edit', 400);
                        }
                        $tag = Tag::get($editing['name'], 'id', 'name', 'description');
                        $tag = array_merge($tag[0], Tag::row($tag[0]['name']));
                        $json_array['status_code'] = 200;
                        $json_array['success'] = [
                            'message' => _s('%s edited', _s('Tag')),
                            'code' => 200,
                        ];
                        $json_array['tag'] = $tag;

                        break;
                    case 'ip_ban':
                        if (! $handler::cond('content_manager')) {
                            throw new Exception('Invalid content owner request', 108);
                        }
                        $id = $REQUEST['editing']['id'];
                        IpBan::validateIP($editing['ip']);
                        if (! empty($editing['expires'])
                            && ! preg_match('/^\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}$/', $editing['expires'])
                        ) {
                            throw new Exception('Invalid expiration date format', 102);
                        }

                        try {
                            if (empty($editing['expires'])) {
                                $editing['expires'] = null;
                            }
                            $editing = array_merge($editing, [
                                'expires_gmt' => $editing['expires'] == null
                                    ? null
                                    : gmdate('Y-m-d H:i:s', strtotime($editing['expires'])),
                            ]);
                            if (! IpBan::update([
                                'id' => $id,
                            ], $editing)) {
                                throw new Exception('Failed to edit IP ban', 400);
                            }
                            $json_array['status_code'] = 200;
                            $json_array['success'] = [
                                'message' => 'IP ban edited',
                                'code' => 200,
                            ];
                            $json_array['ip_ban'] = IpBan::getSingle([
                                'id' => $id,
                            ]);
                        } catch (Exception $throwable) {
                            $json_array = [
                                'status_code' => 403,
                                'error' => [
                                    'message' => $throwable->getMessage(),
                                    $throwable->getCode(),
                                ],
                            ];

                            break;
                        }

                        break;
                    case 'storage':
                        if (! Login::isAdmin()) {
                            throw new Exception('Invalid content owner request', 109);
                        }
                        $id = (int) $REQUEST['editing']['id'];
                        Storage::update($id, $editing);
                        $storage = Storage::getSingle($id);
                        $json_array['status_code'] = 200;
                        $json_array['success'] = [
                            'message' => 'Storage edited',
                            'code' => 200,
                        ];
                        $json_array['storage'] = $storage;

                        break;
                }

                break;
            case 'add-user':
                if (! Login::isAdmin()) {
                    throw new Exception(_s('Request denied'), 403);
                }
                $user = $REQUEST['user'];
                foreach (['username', 'email', 'password', 'role'] as $v) {
                    if (($user[$v] ?? '') === '') {
                        throw new Exception(_s('Missing values'), 100);
                    }
                }
                if (! User::isValidUsername($user['username'])) {
                    throw new Exception(_s('Invalid username'), 101);
                }
                if (! filter_var($user['email'], FILTER_VALIDATE_EMAIL)) {
                    throw new Exception(_s('Invalid email'), 102);
                }
                if (! preg_match('/' . Settings::USER_PASSWORD_PATTERN . '/', $user['password'] ?? '')) {
                    throw new Exception(_s('Invalid password'), 103);
                }
                if (! in_array($user['role'], ['user', 'manager', 'admin'], true)) {
                    throw new Exception(_s('Invalid role'), 104);
                }
                if (DB::get('users', [
                    'username' => $user['username'],
                ])) {
                    throw new Exception(_s('Username already being used'), 200);
                }
                if (DB::get('users', [
                    'email' => $user['email'],
                ])) {
                    throw new Exception(_s('Email already being used'), 200);
                }
                $is_manager = 0;
                $is_admin = 0;
                switch ($user['role']) {
                    case 'manager':
                        $is_manager = 1;

                        break;
                    case 'admin':
                        $is_admin = 1;

                        break;
                }
                $add_user = User::insert([
                    'username' => $user['username'],
                    'email' => $user['email'],
                    'is_admin' => $is_admin,
                    'is_manager' => $is_manager,
                ]);
                if ($add_user) {
                    Login::addPassword($add_user, $user['password'], false);
                }
                $json_array['status_code'] = 200;
                $json_array['success'] = [
                    'message' => _s('%s added', _n('User', 'Users', 1)),
                    'code' => 200,
                ];

                break;
            case 'add-category':
                if (! Login::isAdmin()) {
                    throw new Exception(_s('Request denied'), 403);
                }
                $category = $REQUEST['category'];
                $category_error = false;
                foreach (['name', 'url_key'] as $v) {
                    if (($category[$v] ?? '') === '') {
                        throw new Exception(_s('Missing values'), 100);
                    }
                }
                Category::assertUrlKey($category['url_key'] ?? '');
                if ($handler::var('categories')) {
                    foreach ($handler::var('categories') as $v) {
                        if ($v['url_key'] === $category['url_key']) {
                            $category_error = true;

                            break;
                        }
                    }
                }
                if ($category_error) {
                    throw new Exception(_s('%s URL key already being used.', _s('Category')), 103);
                }
                nullify_string($category['description']);
                $category = array_filter_array($category, ['name', 'url_key', 'description'], 'exclusion');
                assertMaxCount('categories');
                $add_category = DB::insert('categories', $category);
                $category = DB::get('categories', [
                    'id' => $add_category,
                ])[0];
                $category['category_url'] = get_base_url('category/' . $category['category_url_key']);
                $category = DB::formatRow($category);
                $json_array['status_code'] = 200;
                $json_array['success'] = [
                    'message' => _s('%s added', _s('Category')),
                    'code' => 200,
                ];
                $json_array['category'] = $category;
                Categories::deleteCache();

                break;
            case 'add-ip_ban':
                if (! $handler::cond('content_manager')) {
                    throw new Exception(_s('Request denied'), 403);
                }
                $ip_ban = array_filter_array($REQUEST['ip_ban'], ['ip', 'expires', 'message'], 'exclusion');
                IpBan::validateIP($ip_ban['ip']);
                if (! empty($ip_ban['expires'])
                    && ! preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $ip_ban['expires'])
                ) {
                    throw new Exception('Invalid expiration date format', 102);
                }

                try {
                    if (IpBan::getSingle([
                        'ip' => $ip_ban['ip'],
                    ]) !== []) {
                        throw new Exception(_s('IP address already banned'), 103);
                    }
                    if (empty($ip_ban['expires'])) {
                        $ip_ban['expires'] = null;
                    }
                    $ip_ban = array_merge($ip_ban, [
                        'date' => datetime(),
                        'date_gmt' => datetimegmt(),
                        'expires_gmt' => $ip_ban['expires'] == null
                            ? null
                            : gmdate('Y-m-d H:i:s', strtotime($ip_ban['expires'])),
                    ]);
                    $add_ip_ban = IpBan::insert($ip_ban);
                } catch (Exception $throwable) {
                    $json_array = [
                        'status_code' => 403,
                        'error' => [
                            'message' => $throwable->getMessage(),
                            $throwable->getCode(),
                        ],
                    ];

                    break;
                }
                $json_array['status_code'] = 200;
                $json_array['success'] = [
                    'message' => 'IP ban added',
                    'code' => 200,
                ];
                $json_array['ip_ban'] = IpBan::getSingle([
                    'id' => $add_ip_ban,
                ]);

                break;
            case 'add-storage':
                if (! Login::isAdmin()) {
                    throw new Exception(_s('Request denied'), 403);
                }
                $storage = $REQUEST['storage'];
                $add_storage = Storage::insert($storage);
                $storage = Storage::getSingle($add_storage);
                $json_array['status_code'] = 200;
                $json_array['success'] = [
                    'message' => 'Storage added',
                    'code' => 200,
                ];
                $json_array['storage'] = $storage;

                break;
            case 'edit-category':
            case 'flag-safe':
            case 'flag-unsafe':
                if ($logged_user === []) {
                    throw new Exception(_s('Login needed'), 403);
                }
                $editing = $REQUEST['editing'];
                $ownerId = $logged_user['id'];
                $ids = [];
                foreach ($editing['ids'] as $id) {
                    $ids[] = decodeID($id);
                }
                $images = Image::getMultiple($ids);
                $images_ids = [];
                foreach ($images as $image) {
                    if (! $handler::cond('content_manager')
                        && $image['image_user_id'] !== $logged_user['id']
                    ) {
                        continue;
                    }
                    $images_ids[] = $image['image_id'];
                }
                if (! $images_ids) {
                    throw new Exception('Invalid content owner request', 111);
                }
                $prop = null;
                $message = '';
                switch ($doing) {
                    case 'flag-safe':
                    case 'flag-unsafe':
                        if (getSetting('image_lock_nsfw_editing')
                            && ! (Login::isAdmin() || $logged_user['is_manager'])
                        ) {
                            throw new Exception('Invalid request', 403);
                        }
                        $query_field = 'nsfw';
                        $prop = (int) ($editing['nsfw'] ?? 0);
                        $prop = intval($prop === 1);
                        $message = 'Content flag changed';

                        break;
                    case 'edit-category':
                        $query_field = 'category_id';
                        $prop = $editing['category_id'] ?: null;
                        $message = 'Content category edited';

                        break;
                }
                if (! isset($query_field)) {
                    throw new Exception('Invalid request', 403);
                }
                $db = DB::getInstance();
                $db->query('UPDATE `' . DB::getTable('images') . '` SET `image_' . $query_field . '`=:prop WHERE `image_id` IN (' . implode(',', $images_ids) . ')');
                $db->bind(':prop', $prop);
                $db->exec();
                $json_array['status_code'] = 200;
                $json_array['success'] = [
                    'message' => $message,
                    'code' => 200,
                ];
                if ($query_field === 'category_id') {
                    $json_array['category_id'] = $prop;
                }

                break;
            case 'move':
            case 'create-album':
                $type = $REQUEST['type'];
                if (! in_array($type, ['images', 'album', 'albums'], true)) {
                    throw new Exception('Invalid album ' . ($doing === 'move' ? 'move' : 'create') . ' request', 100);
                }
                $album = $REQUEST['album'];
                $album['new'] = $album['new'] == 'true';
                if ($logged_user === [] && $album['new'] === false) {
                    throw new Exception('Invalid request', 403);
                }
                $ownerId = ! empty($REQUEST['owner'])
                    ? decodeID($REQUEST['owner'])
                    : ($logged_user['id'] ?? null);
                if (! $handler::cond('content_manager') && $ownerId !== ($logged_user['id'] ?? null)) {
                    throw new Exception('Invalid content owner request', 112);
                }

                if ($handler::cond('forced_private_mode')) {
                    $album['privacy'] = getSetting('website_content_privacy_mode');
                }
                if (! $handler::cond('content_manager') && getSetting('akismet') && $album['new']) {
                    Akismet::checkAlbum($album['name'], $album['description'], $ownerId === $logged_user['id'] ? $logged_user_source_db : null);
                }
                $album_id = $album['new']
                    ? Album::insert([
                        'name' => $album['name'],
                        'user_id' => $ownerId,
                        'privacy' => $album['privacy'],
                        'description' => $album['description'],
                        'password' => $album['password'] ?? null,
                        'parent_id' => isset($album['parent_id'])
                            ? decodeID($album['parent_id'])
                            : null,
                    ])
                    : decodeID($album['id']);
                $album_db = Album::getSingle(id: $album_id, pretty: false);
                if (isset($album['ids']) && is_array($album['ids'])) {
                    if (count($album['ids']) === 0) {
                        throw new Exception('Invalid source album ids ' . ($doing === 'move' ? 'move' : 'create') . ' request', 100);
                    }
                    $ids = [];
                    foreach ($album['ids'] as $id) {
                        $ids[] = decodeID($id);
                    }
                }
                if (! empty($ids) && is_array($ids)) {
                    if ($type === 'images') {
                        $images = Image::getMultiple($ids);
                        $images_ids = [];
                        foreach ($images as $image) {
                            if ($logged_user === []
                                && in_array($image['image_id'], session()['guest_images'] ?? [], false) === false
                            ) {
                                continue;
                            }
                            if (! $handler::cond('content_manager') && $image['image_user_id'] !== ($logged_user['id'] ?? null)) {
                                continue;
                            }
                            $images_ids[] = $image['image_id'];
                        }
                        if (! $images_ids) {
                            throw new Exception('Invalid content owner request', 104);
                        }
                        Album::addImages(
                            $album_db === []
                                ? null
                                : (int) $album_db['album_id'],
                            $images_ids
                        );
                    } else {
                        $album_move = true;
                        $albums = Album::getMultiple($ids);
                        $albums_ids = [];
                        foreach ($albums as $album) {
                            if (! $handler::cond('content_manager') && $album['album_user_id'] !== $logged_user['id']) {
                                continue;
                            }
                            $albums_ids[] = $album['album_id'];
                        }
                        if (! $albums_ids) {
                            throw new Exception('Invalid content owner request', 105);
                        }
                        Album::moveContents($albums_ids, $album_id);
                    }
                }
                $album_move_db = isset($album_db['album_id'])
                    ? Album::getSingle(id: (int) $album_db['album_id'], pretty: false)
                    : User::getStreamAlbum($ownerId);
                $json_array['status_code'] = 200;
                $json_array['success'] = [
                    'message' => 'Content added to album',
                    'code' => 200,
                ];
                if ($album_move_db !== []) {
                    $json_array['album'] = Album::formatArray($album_move_db, true);
                    $json_array['album']['html'] = Listing::getAlbumHtml($album_move_db['album_id']);
                }
                if ($type === 'albums') {
                    $json_array['albums_old'] = [];
                    foreach ($ids ?? [] as $album_id) {
                        $album_id = (int) $album_id;
                        $album_item = Album::formatArray(
                            Album::getSingle(id: $album_id, pretty: false),
                            true
                        );
                        $album_item['html'] = Listing::getAlbumHtml($album_id);
                        $json_array['albums_old'][] = $album_item;
                    }
                }

                break;
            case 'delete':
                if ($logged_user === []) {
                    throw new Exception(_s('Login needed'), 403);
                }
                $deleting = $REQUEST['deleting'] ?? null;
                $type = $REQUEST['delete'] ?? null;
                if ($type == null) {
                    throw new Exception('Invalid delete request', 100);
                }
                if (! $handler::cond('content_manager')
                    && ! getSetting('enable_user_content_delete')
                    && (starts_with('image', $type) || starts_with('album', $type))
                ) {
                    throw new Exception('Forbidden action', 403);
                }
                $ownerId = isset($REQUEST['owner'])
                    ? decodeID($REQUEST['owner'])
                    : $logged_user['id'];
                $multiple = ($REQUEST['multiple'] ?? null) == 'true';
                $single = ($REQUEST['single'] ?? null) == 'true';
                if (! $multiple) {
                    $single = true;
                }
                if (
                    in_array($type, ['avatar', 'background', 'user', 'ip_ban', 'api_key', 'two_factor'], true)
                    && ! $handler::cond('content_manager') && $ownerId !== $logged_user['id']
                ) {
                    throw new Exception('Invalid content owner request', 113);
                }
                if (
                    in_array($type, ['category', 'storage'], true)
                    && ! $handler::cond('admin')
                ) {
                    throw new Exception('Invalid content admin request', 114);
                }
                if (
                    in_array($type, ['tag'], true)
                    && ! $handler::cond('content_manager')
                ) {
                    throw new Exception('Invalid content manager request', 115);
                }
                if (in_array($type, ['avatar', 'background'], true)) {
                    User::deletePicture($ownerId === $logged_user['id'] ? $logged_user : $ownerId, $type);
                    $json_array['status_code'] = 200;
                    $json_array['success'] = [
                        'message' => 'Profile background deleted',
                        'code' => 200,
                    ];

                    break;
                }
                if ($type === 'two_factor') {
                    $userTarget = intval(
                        $ownerId === $logged_user['id']
                            ? $logged_user['id']
                            : $ownerId
                    );
                    if (! TwoFactor::hasFor($userTarget)) {
                        $status_code = 403;
                        $message = 'Two-factor not enabled';
                    } else {
                        TwoFactor::delete($userTarget);
                        $status_code = 200;
                        $message = 'Two-factor deleted';
                    }
                    $json_array['status_code'] = $status_code;
                    $json_array['success'] = [
                        'message' => $message,
                        'code' => $status_code,
                    ];

                    break;
                }
                if ($type === 'api_key') {
                    $userTarget = intval(
                        $ownerId === $logged_user['id']
                            ? $logged_user['id']
                            : $ownerId
                    );
                    $apiKey = ApiKey::getUserKey($userTarget);
                    if ($apiKey !== []) {
                        ApiKey::remove(intval($apiKey['id']));
                    }
                    $json_array['status_code'] = 200;
                    $json_array['success'] = [
                        'message' => 'API key deleted',
                        'code' => 200,
                    ];

                    break;
                }
                if ($type === 'user') {
                    $delete_user_id = $ownerId === $logged_user['id']
                        ? $logged_user
                        : $ownerId;
                    $delete_user = User::getSingle($delete_user_id, 'id');
                    if ($delete_user === []) {
                        throw new Exception(_s('%s not found', _n('User', 'Users', 1)), 100);
                    }
                    if ($delete_user['is_content_manager'] && Login::isAdmin() === false) {
                        throw new Exception("Can't touch this!", 666);
                    }
                    User::delete($delete_user_id);

                    break;
                }
                if ($single) {
                    if (($deleting['id'] ?? null) == null) {
                        throw new Exception('Missing delete target id', 100);
                    }
                } else {
                    if (is_array($deleting['ids']) && count($deleting['ids']) === 0) {
                        throw new Exception('Missing delete target ids', 100);
                    }
                }
                if ($type === 'category') {
                    if (! array_key_exists($deleting['id'], $handler::var('categories'))) {
                        throw new Exception('Invalid target category', 100);
                    }
                    $delete_category = DB::delete('categories', [
                        'id' => $deleting['id'],
                    ]);
                    if ($delete_category) {
                        DB::update('images', [
                            'category_id' => null,
                        ], [
                            'category_id' => $deleting['id'],
                        ]);
                        Categories::deleteCache();
                    } else {
                        throw new Exception('Error deleting category', 400);
                    }

                    break;
                }
                if ($type === 'tag') {
                    $tagsIds = $multiple
                        ? $deleting['ids']
                        : [$deleting['id']];
                    $delete_tag = Tag::delete(...$tagsIds);
                    if (! $delete_tag) {
                        throw new Exception('Error deleting tag', 400);
                    }

                    break;
                }
                if ($type === 'ip_ban') {
                    if (! IpBan::delete([
                        'id' => $deleting['id'],
                    ])) {
                        throw new Exception('Error deleting IP ban', 400);
                    }

                    break;
                }
                if ($type === 'storage') {
                    Storage::delete($deleting['id']);

                    break;
                }
                if (! in_array($type, ['image', 'album', 'images', 'albums'], true)) {
                    throw new Exception('Invalid delete request', 100);
                }
                $db_field_prefix = in_array($type, ['image', 'images'], true)
                    ? 'image'
                    : 'album';
                switch ($type) {
                    case 'image':
                    case 'images':
                        $Class_fn = Image::class;

                        break;
                    case 'album':
                    case 'albums':
                        $Class_fn = Album::class;

                        break;
                }
                if (! isset($Class_fn)) {
                    throw new Exception('Invalid delete request', 100);
                }
                if ($single) {
                    if (($deleting['id'] ?? '') === '') {
                        throw new Exception('Missing delete target id', 100);
                    }
                    $id = decodeID($deleting['id']);

                    $content_db = $Class_fn::getSingle($id, false, false);
                    if ($content_db) {
                        if (! $handler::cond('content_manager')
                            && $content_db[$db_field_prefix . '_user_id'] != $logged_user['id']
                        ) {
                            throw new Exception('Invalid content owner request', 114);
                        }
                        $delete = $Class_fn::delete($id);
                    } else {
                        throw new Exception("Content doesn't exists", 100);
                    }
                    $affected = $delete;
                } else {
                    if (! is_array($deleting['ids'])) {
                        throw new Exception('Expecting ids array values, ' . gettype($deleting['ids']) . ' given', 100);
                    }
                    $ids = [];
                    foreach ($deleting['ids'] ?? [] as $id) {
                        $ids[] = decodeID($id);
                    }
                    $contents_db = $Class_fn::getMultiple($ids);
                    $owned_ids = [];
                    foreach ($contents_db as $content_db) {
                        if (! $handler::cond('content_manager') and $content_db[$db_field_prefix . '_user_id'] !== $logged_user['id']) {
                            continue;
                        }
                        if (isset($content_db[$db_field_prefix . '_id'])) {
                            $owned_ids[] = $content_db[$db_field_prefix . '_id'];
                        }
                    }
                    if (! $owned_ids) {
                        throw new Exception('Invalid content owner request', 106);
                    }
                    $delete = $Class_fn::deleteMultiple($owned_ids);
                    $affected = $delete;
                }
                $json_array['success'] = [
                    'message' => ucfirst($type) . ' deleted',
                    'code' => 200,
                    'affected' => $affected,
                ];

                break;
            case 'disconnect':
                if ($logged_user === []) {
                    throw new Exception(_s('Login needed'), 403);
                }
                $disconnect = strtolower($REQUEST['disconnect']);
                $disconnect_label = ucfirst($disconnect);
                $user_id = $REQUEST['user_id']
                    ? decodeID($REQUEST['user_id'])
                    : null; // Optional param (allow admin to disconnect any user)
                if (! Login::isAdmin() && $user_id && $user_id !== $logged_user['id']) {
                    throw new Exception('Invalid request', 403);
                }
                $user = ! $user_id ? $logged_user : User::getSingle($user_id, 'id');
                $login_connection = $user['login'][$disconnect] ?? false;
                $providersEnabled = Login::getProviders('enabled');
                if (! array_key_exists($disconnect, $providersEnabled)) {
                    throw new Exception('Invalid disconnect value', 10);
                }
                if (! $login_connection) {
                    throw new Exception("Login connection doesn't exists", 11);
                }
                if ($user['connections_count'] === 1
                    && ! Login::hasPassword($user_id)
                ) {
                    throw new Exception(_s('Add a password or another social connection before deleting %s', $disconnect_label), 12);
                }
                $user_social_conn = 0;
                foreach (array_keys($providersEnabled) as $k) {
                    if (array_key_exists($k, $user['login'])) {
                        ++$user_social_conn;
                    }
                }
                if ($user_social_conn === 1
                    && Login::hasPassword($user['id'])) {
                    if (getSetting('require_user_email_confirmation')
                        && ! $user['email']) {
                        throw new Exception(_s('Add an email or another social connection before deleting %s', $disconnect_label), 12);
                    }
                }
                $loginCookie = 'cookie_' . $disconnect;
                Login::deleteCookies($loginCookie, [
                    'user_id' => $user['id'],
                ]);
                $delete_connection = Login::deleteConnection($disconnect, $user['id']);
                if ($delete_connection) {
                    if (in_array($disconnect, ['twitter', 'facebook'], true)) {
                        User::update($user['id'], [
                            $disconnect . '_username' => null,
                        ]);
                    }
                    $json_array['success'] = [
                        'message' => _s('%s has been disconnected.', $disconnect_label),
                        'code' => 200,
                        'redirect' => '',
                    ];
                    if ($loginCookie === Login::getSession()['type']) {
                        $config = [
                            'callback' => get_public_url('connect/' . $disconnect) . '/',
                            'providers' => [],
                        ];
                        $config['providers'][$disconnect] = [
                            'enabled' => $providersEnabled[$disconnect]['is_enabled'],
                            'keys' => [
                                'id' => $providersEnabled[$disconnect]['key_id'],
                                'secret' => $providersEnabled[$disconnect]['key_secret'],
                            ],
                        ];
                        $session = new HybridauthSession();
                        $hybridauth = new Hybridauth(config: $config, storage: $session);
                        $adapter = $hybridauth->getAdapter($disconnect);
                        if ($adapter->isConnected()) {
                            $adapter->disconnect();
                        }
                        $session->clear();
                        $json_array['success']['redirect'] = get_base_url('login');
                    }
                } else {
                    throw new Exception('Error deleting connection', 666);
                }

                break;
            case 'rebuildStats':
                if (! Login::isAdmin()) {
                    throw new Exception('Invalid request', 403);
                }
                Stat::rebuildTotals();
                $json_array['success'] = [
                    'message' => 'OK',
                    'code' => 200,
                    'redirURL' => get_base_url('dashboard'),
                ];

                break;
            case 'testEmail':
                if (! Login::isAdmin()) {
                    throw new Exception('Invalid request', 403);
                }
                $send_email = send_mail($REQUEST['email'], _s('Test email from %s @ %t', [
                    '%s' => getSetting('website_name'),
                    '%t' => datetime(),
                ]), '<p>' . _s('This is just a test') . '</p>');
                if ($send_email) {
                    $json_array['success'] = [
                        'message' => _s('Test email sent to %s.', $REQUEST['email']),
                        'code' => 200,
                    ];
                } else {
                    $json_array['error'] = [
                        'code' => 500,
                    ];
                }

                break;
            case 'encodeId':
            case 'decodeId':
                if (! Login::isAdmin()) {
                    throw new Exception('Invalid request', 403);
                }
                if ($REQUEST['id'] == null) {
                    throw new Exception('Invalid request', 100);
                }
                $thing = str_replace('Id', '', $doing);
                $id = $REQUEST['id'];
                if ($thing === 'encode') {
                    $res = encodeID((int) $id);
                } else {
                    $res = decodeID($id);
                }
                $json_array['success'] = [
                    'message' => $id . ' == ' . $res,
                    'code' => 200,
                    $thing => $res,
                ];

                break;
            case 'exportUser':
                if (! Login::isAdmin()) {
                    throw new Exception('Invalid request', 403);
                }
                // Validate id
                if ($REQUEST['username'] == null) {
                    throw new Exception(_s('Invalid username'), 100);
                }
                $user = User::getSingle($REQUEST['username'], 'username', false);
                if ($user === []) {
                    throw new Exception(_s('Invalid username'), 101);
                }
                $user = DB::formatRow($user);
                if (! isset($REQUEST['download'])) {
                    $json_array['success'] = [
                        'message' => _s('Downloading %s data', "'" . $user['username'] . "'"),
                        'code' => 200,
                        'redirURL' => get_current_url() . '&action=exportUser&download=1',
                    ];
                } else {
                    $filename = $user['username'] . '.json';
                    $user = array_filter_array($user, ['name', 'username', 'email', 'facebook_username', 'twitter_username', 'website', 'bio', 'timezone', 'language', 'is_private', 'newsletter_subscribe']);
                    $user = json_encode($user, JSON_PRETTY_PRINT);
                    header('Content-type: application/json');
                    header('Content-Disposition: attachment; filename=' . $filename);
                    header('Last-Modified: ' . datetimegmt('D, d M Y H:i:s') . ' UTC');
                    header('Cache-Control: must-revalidate, pre-check=0, post-check=0, max-age=0');
                    header('Pragma: anytextexeptno-cache', true);
                    header('Cache-control: private', false);
                    header('Expires: 0');
                    echo $user;
                    exit();
                }

                break;
            case 'follow':
            case 'unfollow':
                if ($logged_user === []
                    || ! getSetting('enable_followers')
                    || $logged_user['is_private'] === 1
                ) {
                    throw new Exception('Invalid request', 403);
                }
                $follow_array = [
                    'user_id' => $logged_user['id'],
                    'followed_user_id' => decodeID($REQUEST[$doing]['id']),
                ];
                $return = $doing === 'follow'
                    ? Follow::insert($follow_array)
                    : Follow::delete($follow_array);
                if ($return) {
                    unset($return['id']);
                    $json_array['success'] = [
                        'message' => $doing === 'follow'
                            ? _s('%s %u followed', [
                                '%s' => _n('User', 'Users', 1),
                                '%u' => $return['username'],
                            ])
                            : _s('%s %u unfollowed', [
                                '%s' => _n('User', 'Users', 1),
                                '%u' => $return['username'],
                            ]),
                        'code' => 200,
                    ];
                    $json_array['user_followed'] = $return;
                }

                break;
            case 'album-cover-set':
            case 'album-cover-unset':
                if ($logged_user === []) {
                    throw new Exception('Invalid request', 403);
                }
                $image_pub_id = $POST[$doing]['image_id'];
                $album_pub_id = $POST[$doing]['album_id'];
                $image_id = decodeID($image_pub_id);
                $album_id = decodeID($album_pub_id);
                $image = Image::getSingle(id: $image_id, pretty: true);
                if ($image === []) {
                    throw new LogicException(
                        message('Missing image')
                    );
                }
                $album = Album::getSingle($album_id);
                if ($image['album']['id'] !== ($album['id'] ?? 0)) {
                    throw new Exception(_s("%s doesn't belong to this %t", [
                        '%s' => _n('Image', 'Images', 1),
                        '%t' => _n('Album', 'Albums', 1),
                    ]), 100);
                }
                if (isset($logged_user['id'])) {
                    $isLoggedOwner = ($image['user']['id'] ?? null) === $logged_user['id']
                        || ($album['user']['id'] ?? null) === $logged_user['id'];
                } else {
                    $isLoggedOwner = false;
                }
                if (! $handler::cond('content_manager') && ! $isLoggedOwner) {
                    throw new Exception('Invalid content owner request', 101);
                }
                Album::update($album_id, [
                    'cover_id' => $doing === 'album-cover-unset' ? null : $image_id,
                ]);
                $json_array['success'] = [
                    'message' => _s('%s cover updated', _n('Album', 'Albums', 1)),
                    'code' => 200,
                ];

                break;
            case 'like':
            case 'dislike':
                if ($logged_user === [] || ! getSetting('enable_likes')) {
                    throw new Exception('Invalid request', 403);
                }
                $like_array = [
                    'user_id' => $logged_user['id'],
                    'content_id' => decodeID($REQUEST[$doing]['id']),
                    'content_type' => $REQUEST[$doing]['object'],
                ];
                $return = $doing === 'like' ? Like::insert($like_array) : Like::delete($like_array);
                if ($return) {
                    $return['id_encoded'] = encodeID((int) $return['id']);
                    unset($return['id']);
                    $json_array['success'] = [
                        'message' => $doing === 'like' ? _s('Content liked', $return['content']['id_encoded'] ?? '') : _s('Content disliked', $return['content']['id_encoded'] ?? ''),
                        'code' => 200,
                    ];
                    $json_array['content'] = $return;
                }

                break;
            case 'regenStorageStats':
                if (! Login::isAdmin()) {
                    throw new Exception('Invalid request', 403);
                }
                $res = Storage::regenStorageStats($REQUEST['storageId']);
                $json_array['success'] = [
                    'message' => $res,
                    'code' => 200,
                ];

                break;
            case 'migrateStorage':
                if (! Login::isAdmin()) {
                    throw new Exception('Invalid request', 403);
                }
                $res = Storage::migrateStorage($REQUEST['sourceStorageId'], $REQUEST['targetStorageId']);
                $json_array['success'] = [
                    'message' => $res,
                    'code' => 200,
                ];

                break;
            case 'notifications':
                if ($logged_user === []) {
                    throw new Exception('Invalid request', 403);
                }
                $notification_array = [
                    'user_id' => $logged_user['id'],
                ];
                $notifications = Notification::get($notification_array);
                Notification::markAsRead($notification_array);
                $json_array['status_code'] = 200;
                if ($notifications !== []) {
                    $json_array['html'] = '';
                    $template = '<li%class>%avatar<span class="notification-text">%message</span><span class="how-long-ago">%how_long_ago</span></li>';
                    $avatar_src_tpl = [
                        0 => '<span class="user-image default-user-image"><span class="icon fas fa-user-circle"></span></span>',
                        1 => '<img class="user-image" src="%user_avatar_url" alt="%user_name_short_html">',
                    ];
                    $avatar_tpl = [
                        0 => $avatar_src_tpl[0],
                        1 => '<a href="%user_url">%user_avatar</a>',
                    ];
                    foreach ($notifications as $k => $v) {
                        $content_type = $v['content_type'];
                        switch ($v['type']) {
                            case 'like':
                                $message = _s('%u liked your %t %c', [
                                    '%t' => _s($content_type),
                                    '%c' => '<a href="' . $v[$content_type]['url_short'] . '">'
                                        . $v[$content_type][($content_type === 'image' ? 'title' : 'name')
                                        . '_truncated_html'] . '</a>',
                                ]);

                                break;
                            case 'follow':
                                $message = _s('%u is now following you');

                                break;
                        }
                        if (! isset($v['user']['id'])) {
                            continue;
                        }
                        $v['message'] = strtr($message ?? '', [
                            '%u' => $v['user']['is_private'] === 1
                                ? _s('A private user')
                                : ('<a href="' . $v['user']['url'] . '">' . $v['user']['name_short_html'] . '</a>'),
                        ]);
                        if ($v['user']['is_private'] === 1) {
                            $avatar = $avatar_tpl[0];
                        } else {
                            $avatar = strtr($avatar_tpl[1], [
                                '%user_url' => $v['user']['url'],
                                '%user_avatar' => strtr($avatar_src_tpl[isset($v['user']['avatar']) ? 1 : 0], [
                                    '%user_avatar_url' => $v['user']['avatar']['url'] ?? '',
                                    '%user_name_short_html' => $v['user']['name_short_html'],
                                ]),
                            ]);
                        }
                        $json_array['html'] .= strtr($template, [
                            '%class' => ! $v['is_read'] ? ' class="new"' : null,
                            '%avatar' => $avatar,
                            '%user_url' => $v['user']['url'],
                            '%message' => $v['message'],
                            '%how_long_ago' => time_elapsed_string($v['date_gmt']),
                        ]);
                    }
                    unset($content_type);
                } else {
                    $json_array['html'] = null;
                }

                break;
            case 'importStats':
            case 'importEdit':
            case 'importDelete':
            case 'importReset':
            case 'importResume':
                if (($REQUEST['id'] ?? null) == null) {
                    throw new Exception('Missing id parameter', 100);
                }
                $import->id = (int) $REQUEST['id']; // @phpstan-ignore-line
                $import->get(); // @phpstan-ignore-line

                break;
            case 'paletteSet':
                if ($logged_user === [] || ! getSetting('theme_palette_user_select')) {
                    throw new Exception('Invalid request', 403);
                }
                $palette_id = (int) $REQUEST['palette_id'];
                User::update($logged_user['id'], [
                    'palette_id' => $palette_id,
                ]);
                $json_array['status_code'] = 200;
                $logged_user = User::getSingle($logged_user['id']);
                $json_array['palette_id'] = (int) $logged_user['palette_id'];

                break;
            case 'approve':
                if (! (Login::isAdmin() || $logged_user['is_manager'])) {
                    throw new Exception('Invalid request', 403);
                }
                $approve_ids = [];
                $approving = $REQUEST['approving'];
                if (($REQUEST['multiple'] ?? null) == 'true') {
                    $approve_ids = $approving['ids'];
                } else {
                    $approve_ids = [$approving['id']];
                }
                if ($approve_ids === []) {
                    throw new Exception('Missing approve target ids', 600);
                }
                $ids = [];
                foreach ($approve_ids as $value) {
                    $ids[] = decodeID($value);
                }
                $affected = DB::queryExecute(sprintf('UPDATE ' . DB::getTable('images') . ' SET image_is_approved = 1 WHERE image_id IN (%s)', implode(',', $ids)));
                $json_array['status_code'] = 200;
                $json_array['affected'] = $affected;

                break;
            case 'user_ban':
            case 'user_unban':
                if (! $handler::cond('content_manager')) {
                    throw new Exception('Invalid content owner request', 108);
                }
                $user_id = decodeID($REQUEST[$doing]['user_id'] ?? '');
                if ($user_id === 0) {
                    throw new Exception('Invalid user id', 109);
                }
                $user = User::getSingle($user_id);
                if ($user === []) {
                    throw new Exception(_s('%s not found', _n('User', 'Users', 1)), 404);
                }
                User::update($user_id, [
                    'status' => $doing === 'user_ban' ? 'banned' : 'valid',
                ]);
                $json_array['status_code'] = 200;

                break;
            case 'set-license-key':
                if (env()['CHEVERETO_CONTEXT'] === 'saas') {
                    throw new Exception('Not found', 404);
                }
                if (! Login::isAdmin()) {
                    throw new Exception(_s('Request denied'), 403);
                }
                $licenseKey = trim($POST['key'] ?? '');
                if ($licenseKey !== '') {
                    $check = fetch_url(
                        url: 'https://chevereto.com/api/license/check',
                        options: [
                            CURLOPT_POSTFIELDS => http_build_query(
                                [
                                    'license' => $licenseKey,
                                ]
                            ),
                        ]
                    );
                    $check = json_decode($check);
                    if (isset($check->error)) {
                        throw new Exception(
                            $check->error->message,
                            $check->error->code
                        );
                    }
                    $checkVersion = $check->data->version;
                    if (version_compare($checkVersion, '4', '>=') === false) {
                        throw new Exception(
                            _s(
                                'Chevereto V%s license key detected. A Chevereto V%r license key is required.',
                                [
                                    '%s' => $checkVersion,
                                    '%r' => '4',
                                ]
                            ),
                            403
                        );
                    }
                }
                touch(PATH_APP_LICENSE_KEY);
                $licenseContents = "<?php return '{$licenseKey}';";
                if (file_put_contents(PATH_APP_LICENSE_KEY, $licenseContents) !== false) {
                    $json_array['status_code'] = 200;
                    $licenseAction = $licenseKey === ''
                        ? _s('License key removed')
                        : _s('License key updated');
                    $json_array['success'] = [
                        'message' => $licenseAction,
                        'code' => 200,
                    ];
                } else {
                    throw new Exception('Error updating license key', 500);
                }

                break;
            case 'deny':
                throw new Exception(_s('Request denied'), 403);
            default: // EX X
                throw new Exception(
                    ! check_value($doing)
                        ? 'empty action'
                        : "invalid action {$doing}",
                );
        }
        if (isset($import->id)) {
            switch ($doing) {
                case 'importStats':
                    $json_array['status_code'] = 200;
                    $json_array['import'] = $import->parsedImport;

                    break;
                case 'importEdit':
                    if ($REQUEST['values'] === false) {
                        throw new Exception('Missing values parameter', 101);
                    }
                    if (is_array($REQUEST['values']) === false) {
                        throw new Exception('Expecting array values', 102);
                    }
                    $import->edit($REQUEST['values']);
                    $import->get();
                    $json_array['import'] = $import->parsedImport;
                    $json_array['status_code'] = 200;

                    break;
                case 'importReset':
                    $import->reset();
                    $json_array['import'] = $import->parsedImport;
                    $json_array['status_code'] = 200;

                    break;
                case 'importResume':
                    $import->resume();
                    $json_array['import'] = $import->parsedImport;
                    $json_array['status_code'] = 200;

                    break;
                case 'importDelete':
                    $import->delete();
                    $json_array['status_code'] = 200;
                    $json_array['import'] = $import->parsedImport;

                    break;
            }
        }
        if (isset($json_array['success'])
            && ! isset($json_array['status_code'])
        ) {
            $json_array['status_code'] = 200;
        }
        $json_array['request'] = $REQUEST;
    } catch (Throwable $throwable) {
        $throwableHandler = throwableHandler($throwable);
        $docInternal = new PlainDocument($throwableHandler);
        if ($throwable->getCode() < 100 || $throwable->getCode() >= 500) {
            writers()->error()
                ->write($docInternal->__toString() . "\n\n");
            XrThrowableHandler(
                $throwable,
                <<<HTML
                <div class="throwable-message">Incident ID: {$throwableHandler->id()}</div>
                HTML
            );
        }
        $message = Storage::getThrowableMessage($throwable);
        if ($throwable->getCode() !== 0 && $throwable->getCode() !== 999) {
            $message .= ' [Code: ' . $throwable->getCode() . ']';
        }
        $debugLevel = Config::system()->debugLevel();
        $errorCanSurface = $throwable->getCode() === 999
            || ($throwable->getCode() > 99 && $throwable->getCode() < 600);
        $isDebug = in_array($debugLevel, [2, 3], true) || isDebug();
        if (! $isDebug && ! $errorCanSurface) {
            $message = '⭕️ '
                . _s('Something went wrong')
                . ' • '
                . strtr(
                    'Incident ID:%id%',
                    [
                        '%id%' => '' . $throwableHandler->id(),
                    ]
                );
        }

        $json_array = [
            'status_code' => 500,
            'error' => [
                'message' => $message,
                'type' => $throwable::class,
                'time' => $throwableHandler->dateTimeUtc()
                    ->format(DateTimeInterface::ATOM),
                'code' => $throwable->getCode(),
                'id' => $throwableHandler->id(),
            ],
        ];
    }
    json_document_output($json_array);
};
