• Welcome to the Chevereto User Community!

    Here, users from all over the world come together to learn, share, and collaborate on everything related to Chevereto. It's a place to exchange ideas, ask questions, and help improve the software.

    Please keep in mind:

    • This community is user-driven. Always be polite and respectful to others.
    • Support development by purchasing a Chevereto license, which also gives you priority support.
    • Go further by joining the Community Subscription for even faster response times and to help sustain this space
  • Chevereto Support CLST

    Support response

    Support checklist

    • Got a Something went wrong message? Read this guide and provide the actual error. Do not skip this.
    • Confirm that the server meets the System Requirements
    • Check for any available Hotfix - your issue could be already reported/fixed
    • Read documentation - It will be required to Debug and understand Errors for a faster support response

API doesn't work when `guest_uploads` are disabled

bjoern.busch

Chevereto Member
I have updated to 4.2. today in the hope that this would get me around debugging, but now I feel like upload via API is broken, I'm always getting back "Request denied" for both images and videos. If I interpret it correctly, the user isn't determined correctly from the provided API key. I see that here the upload_allowed is being set to false:

https://github.com/chevereto/chevereto/blob/4.2/app/legacy/load/web.php#L557

So the user isn't set. I tried the call via curl and I'm getting the same result:

curl --fail-with-body -X POST \
-H "X-API-Key: chv_SuperSecretKey" \
-H "Content-Type: application/x-www-form-urlencoded" \
https://mygallery.com/api/1/upload?source=https://upload.wikimedia.org/wikipedia/commons/4/41/Sunflower_from_Silesia2.jpg

The key is not the guest key, but from a user account. I recreated it, just in case I messed something up, but no difference.

Any chance that you / someone can give this a try on their instance?
 
Last edited:
Hello, thanks for your concise report on this. I can confirm that the code you mentioned is limiting the API functionality because it restricts the upload process before correctly identifying the user via the API key. I've made some changes to the code that will allow the API to reset these variables based on the detected user. Please give it a try and let me know how it goes!

app/legacy/routes/api.php

PHP:
<?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.
 */
/*
 * This API V1 was introduced in Chevereto V2 and it was carried over to V3.
 *
 * From Chevereto V4 onwards the API versioning follow the major version:
 *
 * - V2 -> API V1
 * - V3 -> API V1
 * - V4 -> API V4 + API V1.1
 */
use Chevereto\Legacy\Classes\Akismet;
use Chevereto\Legacy\Classes\ApiKey;
use Chevereto\Legacy\Classes\Image;
use Chevereto\Legacy\Classes\Settings;
use Chevereto\Legacy\Classes\Upload;
use Chevereto\Legacy\Classes\User;
use Chevereto\Legacy\G\Handler;
use function Chevereto\Legacy\decodeID;
use function Chevereto\Legacy\encodeID;
use function Chevereto\Legacy\G\get_mimetype;
use function Chevereto\Legacy\G\getQsParams;
use function Chevereto\Legacy\G\is_image_url;
use function Chevereto\Legacy\G\is_url;
use function Chevereto\Legacy\G\json_document_output;
use function Chevereto\Legacy\G\json_error;
use function Chevereto\Legacy\G\mime_to_extension;
use function Chevereto\Legacy\G\random_string;
use function Chevereto\Legacy\getSetting;
use function Chevereto\Legacy\getVariable;
use function Chevereto\Vars\env;
use function Chevereto\Vars\files;
use function Chevereto\Vars\request;
use function Chevereto\Vars\server;
return function (Handler $handler) {
    try {
        $user = [];
        $REQUEST = request();
        $FILES = files();
        $SERVER = server();
        $format = $REQUEST['format'] ?? 'json';
        $version = strval($handler->request()[0] ?? '');
        $action = strval($handler->request()[1] ?? '');
        $key = strval($SERVER['HTTP_X_API_KEY'] ?? $REQUEST['key'] ?? '');
        foreach (['version', 'action', 'key'] as $var) {
            if (${$var} === '') {
                throw new Exception("No {$var} provided", 100);
            }
        }
        if (! in_array($version, ['1'], true)) {
            throw new Exception('Invalid API version.', 110);
        }
        $verify = ApiKey::verify($key);
        if ($verify === []) {
            if (! (bool) env()['CHEVERETO_ENABLE_API_GUEST']) {
                throw new Exception('Invalid API key.', 100);
            }
            $apiV1Key = (string) (getSetting('api_v1_key') ?? '');
            if ($apiV1Key === '') {
                throw new Exception("API V1 public key can't be null. Go to /dashboard and set the Guest API key.", 0);
            }
            if (! hash_equals($apiV1Key, $key)) {
                throw new Exception('Invalid API key.', 100);
            }
        } else {
            $user = User::getSingle($verify['user_id']);
        }
        $isAdmin = boolval(($user['is_admin'] ?? false));
        $upload_enabled = $isAdmin ?: getSetting('enable_uploads');
        $upload_allowed = $upload_enabled;
        if ($user === []) {
            if (! getSetting('guest_uploads')
                || getSetting('website_privacy_mode') === 'private'
                || $handler::cond('maintenance')
            ) {
                $upload_allowed = false;
            }
        } elseif (! $isAdmin
            && getSetting('website_mode') === 'personal'
            && getSetting('website_mode_personal_uid') !== ($user['id'] ?? 0)
        ) {
            $upload_allowed = false;
        }
        if ((! (bool) env()['CHEVERETO_ENABLE_LOCAL_STORAGE'])
            && getVariable('storages_active')->nullInt() === 0
        ) {
            $upload_enabled = false;
            $upload_allowed = false;
        }
        if ($user === []
            && $upload_allowed
            && getSetting('upload_max_filesize_mb_guest')
        ) {
            Settings::setValue('upload_max_filesize_mb_bak', getSetting('upload_max_filesize_mb'));
            Settings::setValue('upload_max_filesize_mb', getSetting('upload_max_filesize_mb_guest'));
        }
        $handler::setCond('upload_enabled', $upload_enabled);
        $handler::setCond('upload_allowed', $upload_allowed);
        if (Settings::get('enable_uploads_url') && ! $isAdmin) {
            Settings::setValue('enable_uploads_url', 0);
        }
        if (! $handler::cond('upload_allowed')) {
            throw new Exception(_s('Request denied'), 401);
        }
        $version_to_actions = [
            '1' => ['upload'],
        ];
        if (! in_array($action, $version_to_actions[$version], true)) {
            throw new Exception('Invalid API action', 120);
        }
        $source = $FILES['source']
            ?? $REQUEST['source']
            ?? $REQUEST['image']
            ?? null;
        if ($source === null) {
            throw new Exception('Empty upload source', 130);
        }
        switch (true) {
            case isset($FILES['source'], $FILES['source']['tmp_name']):
                $source = $FILES['source'];
                break;
            case is_image_url($source) || is_url($source):
                if (($SERVER['REQUEST_METHOD'] ?? '') === 'GET') {
                    $sourceQs = urldecode(getQsParams()['source']);
                }
                $source = $sourceQs ?? $source;
                break;
            default:
                if (($SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
                    throw new Exception('Upload using base64 source must be done using POST method.', 130);
                }
                $source = trim(preg_replace('/\s+/', '', $source));
                $base64source = base64_encode(base64_decode($source, true));
                if (! hash_equals($base64source, $source)) {
                    throw new Exception('Invalid base64 string', 120);
                }
                if ($source === '') {
                    throw new Exception('Empty source', 130);
                }
                try {
                    $api_temp_file = Upload::getTempNam();
                } catch (Exception $e) {
                    throw new Exception("Can't get a tempnam", 200);
                }
                $fh = fopen($api_temp_file, 'w');
                stream_filter_append($fh, 'convert.base64-decode', STREAM_FILTER_WRITE);
                fwrite($fh, $source);
                fclose($fh);
                $mimetype = get_mimetype($api_temp_file);
                $source = [
                    'name' => random_string(12) . '.' . mime_to_extension($mimetype),
                    'type' => $mimetype,
                    'tmp_name' => $api_temp_file,
                    'error' => 'UPLOAD_ERR_OK',
                    'size' => filesize($api_temp_file),
                ];
                break;
        }
        $isImgBBSpec = array_key_exists('image', $REQUEST);
        $albumId = $REQUEST['album_id'] ?? null;
        if ($albumId !== null) {
            $albumId = decodeID($albumId);
        }
        $expiration = $REQUEST['expiration'] ?? null;
        if ($expiration !== null && ctype_digit($expiration)) {
            $expiration = (int) $expiration;
        }
        $params = [
            'album_id' => $albumId,
            'category_id' => $REQUEST['category_id'] ?? null,
            'description' => $REQUEST['description'] ?? null,
            'nsfw' => $REQUEST['nsfw'] ?? null,
            'title' => $REQUEST['title'] ?? $REQUEST['name'] ?? null,
            'tags' => $REQUEST['tags'] ?? null,
            'width' => $REQUEST['width'] ?? null,
            'expiration' => $expiration,
        ];
        $params = array_filter($params);
        if (! $handler::cond('content_manager') && getSetting('akismet')) {
            $user_source_db = [
                'user_name' => $user['name'] ?? null,
                'user_username' => $user['username'] ?? null,
                'user_email' => $user['email'] ?? null,
            ];
            Akismet::checkImage($params['title'] ?? null, $params['description'] ?? null, $user_source_db);
        }
        $uploadToWebsite = Image::uploadToWebsite($source, $user, $params);
        $uploaded_id = intval($uploadToWebsite[0]);
        $image = Image::formatArray(Image::getSingle($uploaded_id), true);
        $image['delete_url'] = Image::getDeleteUrl(
            type: $image['type'],
            idEncoded: encodeID($uploaded_id),
            password: $uploadToWebsite[1]
        );
        unset($image['user'], $image['album']);
        if (! $image['is_approved']) {
            unset($image['image']['url'], $image['thumb']['url'], $image['medium']['url'], $image['url'], $image['display_url']);
        }
        $json_array = [];
        $json_array['status_code'] = 200;
        if ($isImgBBSpec) {
            $json_array['status'] = $json_array['status_code'];
            $image['id'] = $image['id_encoded'];
        }
        $json_array['success'] = [
            'message' => 'file uploaded',
            'code' => 200,
        ];
        $json_array[$isImgBBSpec ? 'data' : 'image'] = $image;
        if ($version === '1') {
            switch ($format) {
                default:
                case 'json':
                    json_document_output($json_array);
                    break;
                case 'txt':
                    echo $image['url'];
                    break;
                case 'redirect':
                    if ($json_array['status_code'] === 200) {
                        $redirect_url = $image['path_viewer'];
                        header("Location: {$redirect_url}");
                    } else {
                        exit($json_array['status_code']);
                    }
                    break;
            }
            exit();
        }
        json_document_output($json_array);
    } catch (Exception $e) {
        $json_array = json_error($e);
        if ($version === '1') {
            switch ($format) {
                default:
                case 'json':
                    json_document_output($json_array);
                    break;
                case 'txt':
                case 'redirect':
                    exit($json_array['error']['message']);
            }
        } else {
            json_document_output($json_array);
        }
    }
};
 
ok, the mapping of the API Key is now solved, but I think there is a subsequent issue in https://github.com/chevereto/chevereto/blob/4.2/app/src/Legacy/Classes/Image.php#L675

When I call the API like this:
Code:
curl --location --request POST 'https://mygallery.com/api/1/upload?source=https%3A%2F%2Fupload.wikimedia.org%2Fwikipedia%2Fcommons%2F4%2F41%2FSunflower_from_Silesia2.jpg' \
--header 'X-API-Key: chv_neU_SuperSecretKey'

The API Key is properly extracted and mapped to the user, the source parameter is found and it tries to call fetch_url (https://github.com/chevereto/chevereto/blob/4.2/app/src/Legacy/Classes/Image.php#L725) with:

Code:
file: /var/www/html/images/2024/10/30/
url: https://upload.wikimedia.org/wikipedia/commons/4/41/Sunflower_from_Silesia2.jpg

The method then fails somewhere with fopen(\/var\/www\/html\/images\/2024\/10\/30\/): Failed to open stream: Is a directory. I tried creating the folder manually, but that doesn't help. I'm still debugging, but maybe you see the issue immediately.
 
Awesome, I think we have it now! I could now successfully upload a jpg and a mp4 via the API, without specifying a mimetype in the call, but it was automatically detected. Time to pop the champagne!
 
Back
Top