<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    galleries
 */

/*EXTRA FUNCTIONS: shell_exec*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__galleries2()
{
    if (!defined('DEFAULT_AUDIO_WIDTH')) {
        define('DEFAULT_AUDIO_WIDTH', 300);
        define('DEFAULT_AUDIO_HEIGHT', 60);
    }
}

/**
 * Get image details from POST environment and details/metadata.
 * Allows required fields to be found in metadata before an error is raised.
 *
 * @param  boolean $is_edit Whether this is for an edit operation
 * @param  URLPATH $url URL to the image (blank: not known yet, calculate here if possible)
 * @param  ?string $filename Filename of image (null: not known yet, calculate here if possible)
 * @param  string $title Title of image (blank: not known yet, calculate here if possible)
 * @param  string $cat Gallery (blank: not known yet, calculate here if possible)
 * @return array Tuple of image details
 */
function image_get_defaults__post(bool $is_edit = false, string $url = '', ?string $filename = null, string $title = '', string $cat = '') : array
{
    if (($is_edit) && (fractional_edit())) {
        if ($title == '') {
            $title = post_param_string('title', '');
        }
        if (($title == '') && (get_option('gallery_media_title_required') != '0')) {
            warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', 'title'));
        }

        return [
            STRING_MAGIC_NULL,
            STRING_MAGIC_NULL,
            $title,
            STRING_MAGIC_NULL,
        ];
    }

    require_code('uploads');

    if ($url == '') {
        $_filename = '';
        require_code('images2');
        $_url = post_param_image('image', 'uploads/galleries', null, false, $is_edit, $_filename);
        if ($url == '') {
            if ($_url != '') {
                $url = $_url;
            }
        }
        if ($url == '') {
            warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN_UPLOAD'));
        }
        if ($filename === null) {
            if ($_filename != '') {
                $filename = $_filename;
            }
        }
    }

    if ($title == '') {
        $title = post_param_string('title', '');
    }

    if ($cat == '') {
        $cat = post_param_string('cat', '');
    }

    list(
        $title,
        $cat,
    ) = image_get_default_metadata($url, $filename, $title, $cat);
    if (($title == '') && (get_option('gallery_media_title_required') != '0')) {
        warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', 'title'));
    }
    if ($cat == '') {
        warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', 'cat'));
    }

    return [
        $url,
        $filename,
        $title,
        $cat,
    ];
}

/**
 * Get image details from metadata.
 *
 * @param  URLPATH $url URL to the image (blank: not known yet)
 * @param  ?string $filename Filename of image (null: not known yet)
 * @param  string $title Title of image (blank: not known yet, calculate here if possible)
 * @param  string $cat Gallery (blank: not known yet, calculate here if possible)
 * @return array Tuple of image details
 */
function image_get_default_metadata(string $url = '', ?string $filename = null, string $title = '', string $cat = '') : array
{
    if (($filename === null) && ($url != '')) {
        $filename = rawurldecode(basename($url));
    }

    if ($title == '') {
        if ($url != '') {
            $path = convert_url_to_path($url);
            if ($path !== null) {
                require_code('exif');
                $exif = get_exif_data($path, $filename);
                if ($exif['UserComment'] != '') {
                    $title = $exif['UserComment'];
                }
            }
        }
    }

    if ($cat == '') {
        $cat = 'root';
    }

    return [
        $title,
        $cat,
        /*$filename,*/
    ];
}

/**
 * Get video details from POST environment and details/metadata.
 *
 * @param  boolean $is_edit Whether this is for an edit operation
 * @param  URLPATH $url URL to the video (blank: not known yet, calculate here if possible)
 * @param  URLPATH $thumb_url URL to the thumbnail image (blank: not known yet, calculate here if possible)
 * @param  ?string $filename Filename of video (null: not known yet, calculate here if possible)
 * @param  ?integer $video_width Video width (null: not known yet, calculate here if possible)
 * @param  ?integer $video_height Video height (null: not known yet, calculate here if possible)
 * @param  ?integer $video_length Video length (null: not known yet, calculate here if possible)
 * @param  URLPATH $closed_captions_url Video width (blank: not known yet, calculate here if possible)
 * @param  string $title Title of video (blank: not known yet, calculate here if possible)
 * @param  string $cat Gallery (blank: not known yet, calculate here if possible)
 * @return array Tuple of video details
 */
function video_get_defaults__post(bool $is_edit = false, string $url = '', string $thumb_url = '', ?string $filename = null, ?int $video_width = null, ?int $video_height = null, ?int $video_length = null, string $closed_captions_url = '', string $title = '', string $cat = '') : array
{
    if (($is_edit) && (fractional_edit())) {
        $title = post_param_string('title', '');
        if (($title == '') && (get_option('gallery_media_title_required') != '0')) {
            warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', 'title'));
        }

        return [
            STRING_MAGIC_NULL,
            STRING_MAGIC_NULL,
            STRING_MAGIC_NULL,
            INTEGER_MAGIC_NULL,
            INTEGER_MAGIC_NULL,
            INTEGER_MAGIC_NULL,
            STRING_MAGIC_NULL,
            $title,
            STRING_MAGIC_NULL,
        ];
    }

    require_code('uploads');

    if (($url == '') || ($thumb_url == '')) {
        $_filename = '';
        $_thumb_url = '';
        $_url = post_param_multi_source_upload('video', 'uploads/galleries', false, $is_edit, $_filename, $_thumb_url, CMS_UPLOAD_VIDEO);
        if ($url == '') {
            if ($_url != '') {
                $url = $_url;
            }
        }
        if ($url == '') {
            warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN_UPLOAD'));
        }
        if ($thumb_url == '') {
            if ($_thumb_url != '') {
                $thumb_url = $_thumb_url;
            }
        }
        if ($filename === null) {
            if ($_filename != '') {
                $filename = $_filename;
            }
        }
    }

    $thumb_url = video_get_default_thumb_url($url, $thumb_url, $filename);
    if ($thumb_url == '') {
        warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN_UPLOAD'));
    }

    if ($title == '') {
        $title = post_param_string('title', '');
    }

    if ($cat == '') {
        $cat = post_param_string('cat', '');
    }

    list(
        $video_width,
        $video_height,
        $video_length,
        $title,
        $cat,
    ) = video_get_default_metadata($url, $thumb_url, $filename, $video_width, $video_height, $video_length, $title, $cat);

    if (($title == '') && (get_option('gallery_media_title_required') != '0')) {
        warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', 'title'));
    }
    if ($cat == '') {
        warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', 'cat'));
    }

    $closed_captions_url = post_param_multi_source_upload('closed_captions_url', 'uploads/galleries', false, $is_edit);

    if ($closed_captions_url == '') {
        if ($url != '') {
            require_code('images');
            $closed_captions_url = get_matching_closed_captions_file($url);
            if ($closed_captions_url === null) {
                $closed_captions_url = '';
            }
        }
    }

    return [
        $url,
        $thumb_url,
        $filename,
        $video_width,
        $video_height,
        $video_length,
        $closed_captions_url,
        $title,
        $cat,
    ];
}

/**
 * Create a video thumbnail.
 *
 * @param  URLPATH $url URL to the video (blank: not known yet)
 * @param  URLPATH $thumb_url URL to the thumbnail image (blank: not known yet, calculate here if possible)
 * @param  ?string $filename Filename of video (null: not known yet, calculate here if possible)
 * @return URLPATH Thumbnail URL (blank: could not generate)
 */
function video_get_default_thumb_url(string $url = '', string $thumb_url = '', ?string $filename = null) : string
{
    require_code('crypt');
    require_code('images');
    require_code('files');
    require_code('media_renderer');
    require_code('http');

    if ($url != '') {
        if ($filename === null) {
            if ($url != '') {
                $filename = rawurldecode(basename($url));
            }
        }
        $image_filename = null;
        if ($filename !== null) {
            $image_filename = basename($filename, '.' . get_file_extension($filename)) . '.png';
        }
        if (empty($image_filename)) {
            $image_filename = get_secure_random_string() . '.png';
        }

        // Try to find a hook that can get a thumbnail easily
        $hooks = find_media_renderers($url, [], true, null);
        if ($hooks !== null) {
            foreach ($hooks as $hook) {
                $ve_ob = object_factory('Hook_media_rendering_' . filter_naughty_harsh($hook));
                if (method_exists($ve_ob, 'get_video_thumbnail')) {
                    $ret = $ve_ob->get_video_thumbnail($url);
                    if ($ret !== null) {
                        return $ret;
                    }
                }
            }
        }

        // oEmbed etc
        $meta_details = get_webpage_meta_details($url);
        if ($meta_details['t_image_url'] != '') {
            return $meta_details['t_image_url'];
        }

        // Audio default
        if (is_audio($filename, true, true)) {
            return find_theme_image('audio_thumb', true);
        }

        // Ok, gonna try hard using what FFMPEG techniques we can...

        $path = convert_url_to_path($url);
        if ($path !== null) {
            if (class_exists('ffmpeg_movie')) {
                $movie = @(new ffmpeg_movie($path, false));
                if ($movie !== false) {
                    if ($movie->getFrameCount() == 0) {
                        return '';
                    }

                    $frame = $movie->getFrame(min($movie->getFrameCount(), 25));
                    $gd_img = $frame->toGDImage();

                    list($output_path, $thumb_url) = find_unique_path('uploads/galleries_thumbs', filter_naughty($filename), true);

                    if (method_exists($frame, 'toGDImage')) {
                        $gd_img = $frame->toGDImage();
                        @imagepng($gd_img, $output_path);
                    } else {
                        $frame->save($output_path); // New-style
                    }

                    if (file_exists($output_path)) {
                        convert_image($output_path, $output_path, null, null, intval(get_option('thumb_width')), true, null, true);
                        return $thumb_url;
                    }
                }
            }

            $ffmpeg_path = get_option('ffmpeg_path');
            if (($ffmpeg_path != '') && (php_function_allowed('shell_exec'))) {
                list($output_path, $thumb_url) = find_unique_path('uploads/galleries_thumbs', filter_naughty($filename), true);

                $at = '00:00:01';
                $shell_command = '"' . $ffmpeg_path . 'ffmpeg" -i ' . cms_escapeshellarg($path) . ' -an -ss ' . $at . ' -r 1 -vframes 1 -y ' . cms_escapeshellarg($output_path);

                $shell_commands = [$shell_command, $shell_command . ' -map 0.0:0.0', $shell_command . ' -map 0.1:0.0'];
                foreach ($shell_commands as $shell_command) {
                    shell_exec($shell_command);
                    if (@filesize($output_path)) {
                        break;
                    }
                }

                if (file_exists($output_path)) {
                    convert_image($output_path, $output_path, null, null, intval(get_option('thumb_width')), true, null, true);
                    return $thumb_url;
                }
            }
        }
    } else {
        // Audio default
        if (is_audio($filename, true, true)) {
            return find_theme_image('audio_thumb', true);
        }
    }

    // Default
    return find_theme_image('video_thumb', true);
}

/**
 * Get video details from metadata.
 *
 * @param  URLPATH $url URL to the video (blank: not known yet)
 * @param  URLPATH $thumb_url URL to the thumbnail image (blank: not known yet)
 * @param  ?string $filename Filename of video (null: not known yet, calculate here if possible)
 * @param  ?integer $video_width Video width (null: not known yet, calculate here if possible)
 * @param  ?integer $video_height Video height (null: not known yet, calculate here if possible)
 * @param  ?integer $video_length Video length (null: not known yet, calculate here if possible)
 * @param  string $title Title of video (blank: not known yet, calculate here if possible)
 * @param  string $cat Gallery (blank: not known yet, calculate here if possible)
 * @return array Tuple of video details
 */
function video_get_default_metadata(string $url = '', string $thumb_url = '', ?string $filename = null, ?int $video_width = null, ?int $video_height = null, ?int $video_length = null, string $title = '', string $cat = '') : array
{
    if ($url != '') {
        if (($video_width === null) || ($video_height === null) || ($video_length === null)) {
            $path = convert_url_to_path($url);
            if ($path === null) {
                $download_test = null;
                $temp_path = null;
                if ($url != '') {
                    $temp_path = cms_tempnam();
                    $write_to_file = fopen($temp_path, 'wb');
                    $download_test = cms_http_request($url, ['byte_limit' => 1024 * 50, 'trigger_error' => false, 'write_to_file' => $write_to_file]);
                    rewind($write_to_file);
                    fclose($write_to_file);
                }
                if (($download_test !== null) && ($download_test->data !== null)) {
                    if ($download_test->filename !== null) {
                        $filename = $download_test->filename;
                    }
                    list(
                        $meta_video_width,
                        $meta_video_height,
                        $meta_video_length,
                    ) = get_video_details_from_file($temp_path, $filename);
                } else {
                    list(
                        $meta_video_width,
                        $meta_video_height,
                        $meta_video_length,
                    ) = [null, null, null];
                }

                if ($temp_path !== null) {
                    @unlink($temp_path);
                }
            } else {
                list(
                    $meta_video_width,
                    $meta_video_height,
                    $meta_video_length,
                ) = get_video_details_from_file($path, $filename, true);
            }

            if ($video_width === null) {
                if ($meta_video_width !== null) {
                    $video_width = $meta_video_width;
                }
            }
            if ($video_height === null) {
                if ($meta_video_height !== null) {
                    $video_height = $meta_video_height;
                }
            }
            if ($video_length === null) {
                if ($meta_video_length !== null) {
                    $video_length = $meta_video_length;
                }
            }
        }

        if (($video_width === null) || ($video_height === null)) {
            // Try oEmbed
            require_code('media_renderer');
            require_code('http');
            $meta_details = get_webpage_meta_details($url);
            require_code('hooks/systems/media_rendering/oembed');
            $oembed_ob = object_factory('Hook_media_rendering_oembed');
            if ((!empty($meta_details['t_mime_type'])) && ($oembed_ob->recognises_mime_type($meta_details['t_mime_type'], $meta_details)) || $oembed_ob->recognises_url($url)) {
                $oembed = $oembed_ob->get_oembed_data_result($url, ['width' => get_option('video_width_setting'), 'height' => get_option('video_height_setting')]);
                if (isset($oembed['width'])) {
                    if ($video_width === null) {
                        $video_width = intval($oembed['width']);
                    }
                }
                if (isset($oembed['height'])) {
                    if ($video_height === null) {
                        $video_height = intval($oembed['height']);
                    }
                }
            }
        }

        if ($filename === null) {
            $filename = rawurldecode(basename($url));
        }
    }

    if ($filename !== null) {
        if (($video_width === null) || ($video_height === null)) {
            require_code('images');
            $is_audio = is_audio($filename, true, true);
        } else {
            $is_audio = false;
        }
    } else {
        $is_audio = false;
    }

    if ($video_width === null) {
        if ($is_audio) {
            $video_width = DEFAULT_AUDIO_WIDTH;
        } else {
            $video_width = intval(get_option('default_video_width'));
        }
    }
    if ($video_height === null) {
        if ($is_audio) {
            $video_height = DEFAULT_AUDIO_HEIGHT;
        } else {
            $video_height = intval(get_option('default_video_height'));
        }
    }
    if ($video_length === null) {
        $video_length = 0;
    }

    if ($title == '') {
        if ($url != '') {
            $path = convert_url_to_path($url);
            if ($path !== null) {
                require_code('exif');
                $exif = get_exif_data($path, $filename);
                if ($exif['UserComment'] != '') {
                    $title = $exif['UserComment'];
                }
            }
        }
    }

    if ($cat == '') {
        $cat = 'root';
    }

    return [
        $video_width,
        $video_height,
        $video_length,
        $title,
        $cat,
    ];
}

/**
 * Get width,height,length of a video file. Note: unfortunately mpeg is not possible without huge amounts of code.
 *
 * @param  PATH $file_path The path to the video file
 * @param  ?string $filename The original filename of the video file (so we can find the file type from the file extension) (null: derive from $file_path)
 * @param  boolean $delay_errors Whether to skip over errored files instead of dying. We don't currently make use of this as our readers aren't sophisticated enough to properly spot erroneous situations.
 * @return ~array The triplet of width/height/length (possibly containing nulls for when we can't detect properties) (false: error)
 */
function get_video_details_from_file(string $file_path, ?string $filename, bool $delay_errors = false)
{
    $info = null;

    if ($filename === null) {
        $filename = basename($file_path);
    }

    $extension = get_file_extension($filename);

    $file = @fopen($file_path, 'rb');
    if ($file === false) {
        return false;
    }
    flock($file, LOCK_SH);

    switch ($extension) {
        case 'qt':
        case 'mov':
        case 'f4v':
        case 'mp4':
        case 'm4v':
            $info = _get_mp4_details($file);
            break;
        default:
            if ((file_exists(get_file_base() . '/sources_custom/getid3/getid3.php')) && (!in_safe_mode())) {
                error_reporting(0);

                if (!defined('GETID3_HELPERAPPSDIR')) {
                    define('GETID3_HELPERAPPSDIR', get_file_base() . '/sources_custom/getid3/helperapps');
                }

                $before = ini_get('ocproducts.type_strictness');
                cms_ini_set('ocproducts.type_strictness', '0');

                require_code('getid3/getid3');
                if (class_exists('getID3')) {
                    $id3_ob = new getID3();
                    $_info = $id3_ob->analyze($file_path);
                    $info = [
                        isset($_info['video']['resolution_x']) ? $_info['video']['resolution_x'] : null,
                        isset($_info['video']['resolution_y']) ? $_info['video']['resolution_y'] : null,
                        array_key_exists('playtime_seconds', $_info) ? intval($_info['playtime_seconds']) : null,
                    ];
                    if (isset($_info['meta']['onMetaData']['width'])) {
                        $info[0] = intval($_info['meta']['onMetaData']['width']);
                    }
                    if (isset($_info['meta']['onMetaData']['height'])) {
                        $info[1] = intval($_info['meta']['onMetaData']['height']);
                    }

                    require_code('mime_types');
                    $mime_type = get_mime_type($extension, true);
                    if (substr($mime_type, 0, 6) == 'audio/') {
                        $info[0] = null;
                        $info[1] = null;
                    }
                }

                cms_ini_set('ocproducts.type_strictness', $before);
            }
            break;
    }

    flock($file, LOCK_UN);
    fclose($file);

    if ($info === null) {
        return [null, null, null];
    }
    return $info;
}

/**
 * Read an integer from the given binary chunk. The integer is in intel endian form.
 *
 * @param  string $buffer The binary chunk
 * @return integer The integer
 */
function read_intel_endian_int(string $buffer) : int
{
    if (strlen($buffer) == 2) {
        return ord($buffer[0]) | (ord($buffer[1]) << 8);
    }
    if (strlen($buffer) < 4) {
        warn_exit(do_lang_tempcode('CORRUPT_FILE', do_lang('VIDEO'))); // Error
    }
    return ord($buffer[0]) | (ord($buffer[1]) << 8) | (ord($buffer[2]) << 16) | (ord($buffer[3]) << 24);
}

/**
 * Read an integer from the given binary chunk. The integer is in network endian form.
 *
 * @param  string $buffer The binary chunk
 * @return integer The integer
 */
function read_network_endian_int(string $buffer) : int
{
    if (strlen($buffer) == 2) {
        return ord($buffer[1]) | (ord($buffer[0]) << 8);
    }
    if (strlen($buffer) < 4) {
        warn_exit(do_lang_tempcode('CORRUPT_FILE', do_lang('VIDEO'))); // Error
    }
    return ord($buffer[3]) | (ord($buffer[2]) << 8) | (ord($buffer[1]) << 16) | (ord($buffer[0]) << 24);
}

/**
 * Get width,height,length of a .mp4 video file.
 *
 * @param  resource $file The file handle
 * @return ?array The triplet (possibly containing nulls for when we can't detect properties) (null: error)
 * @ignore
 */
function _get_mp4_details($file) : ?array
{
    // Read in atoms
    $info = _get_mp4_details_do_atom_list($file);
    if ($info === null) {
        return null;
    }
    list($_, $width, $height, $length) = $info;
    return [$width, $height, $length];
}

/**
 * Get chunk-bytes-read,width,height,length of a atom list of a .mp4 video file.
 *
 * @param  resource $file The file handle
 * @param  ?integer $atom_size The length of the current atom list (null: covers full file)
 * @return array The quartet (possibly containing nulls for when we can't detect properties)
 * @ignore
 */
function _get_mp4_details_do_atom_list($file, ?int $atom_size = null) : array
{
    $length = null;
    $width = null;
    $height = null;

    $count = 0;
    while ((!feof($file)) && (($atom_size === null) || ($count < $atom_size)) && (($length === null) || ($width === null) || ($height === null))) {
        $next_read = fread($file, 4);
        if (strlen($next_read) < 4) {
            return [$count, $width, $height, $length]; // END / problem
        }
        $size = read_network_endian_int($next_read);
        if ($size < 8) { // NB: uuid atom can be of size 8 (i.e. empty) on some rare files
            return [$count, $width, $height, $length]; // END / problem
        }
        $count += 4;
        if ($size == 0) {
            //$qt_atom = true;
            fseek($file, 8, SEEK_CUR);
            $size = read_network_endian_int(fread($file, 4));
            $count += 12;
        }
        // else $qt_atom = false;

        $type = fread($file, 4);
        $count += 4;
        if ($type == 'mvhd') {
            fseek($file, 12, SEEK_CUR);
            $time_scale = read_network_endian_int(fread($file, 4));
            $duration = read_network_endian_int(fread($file, 4));
            if ($time_scale == 0) {
                return [$count, $width, $height, $length]; // problem
            }
            $length = intval(round(floatval($duration) / floatval($time_scale)));
            fseek($file, 80, SEEK_CUR);
            $count += 20 + 80;
        } elseif ($type == 'tkhd') {
            fseek($file, 76, SEEK_CUR);
            $_width = read_network_endian_int(fread($file, 2));
            fseek($file, 2, SEEK_CUR); // fixed point - but we don't want decimal fraction part
            $_height = read_network_endian_int(fread($file, 2));
            fseek($file, 2, SEEK_CUR); // fixed point - but we don't want decimal fraction part
            $count += 76 + 8;
            if ($_width > 0) {
                $width = $_width;
            }
            if ($_height > 0) {
                $height = $_height;
            }
        } elseif ($type == 'moov') { // moov contains more atoms, and the one we need for length
            $info = _get_mp4_details_do_atom_list($file, $size - $count);
            $count += $info[0];
            if ($info[1] !== null) {
                $width = $info[1];
            }
            if ($info[2] !== null) {
                $height = $info[2];
            }
            if ($info[3] !== null) {
                $length = $info[3];
            }
        } elseif ($type == 'trak') { // trak contains more atoms, and the one we need for width and height
            $info = _get_mp4_details_do_atom_list($file, $size - $count);
            $count += $info[0];
            if ($info[1] !== null) {
                $width = $info[1];
            }
            if ($info[2] !== null) {
                $height = $info[2];
            }
            if ($info[3] !== null) {
                $length = $info[3];
            }
        } else {
            fseek($file, $size - 8, SEEK_CUR);
            $count += $size - 8;
        }
    }

    return [$count, $width, $height, $length];
}

/**
 * Decache gallery blocks.
 */
function decache_gallery_blocks()
{
    if (!running_script('install')) {
        delete_cache_entry('side_galleries');
        delete_cache_entry('main_personal_galleries_list');
        delete_cache_entry('main_gallery_embed');
        delete_cache_entry('main_gallery_mosaic');
        delete_cache_entry('main_image_fader');
        delete_cache_entry('main_image_slider');
        delete_cache_entry('main_hero_slider');
    }
}

/**
 * Add an image to a specified gallery.
 *
 * @param  mixed $title Image title (either language string map or string)
 * @param  ID_TEXT $cat The gallery name
 * @param  mixed $description The image description (either language string map or string)
 * @param  URLPATH $url The URL to the actual image
 * @param  BINARY $validated Whether the image has been validated for display on the site
 * @param  BINARY $allow_rating Whether the image may be rated
 * @param  SHORT_INTEGER $allow_comments Whether the image may be commented upon
 * @param  BINARY $allow_trackbacks Whether the image may be trackbacked
 * @param  LONG_TEXT $notes Hidden notes associated with the image
 * @param  ?MEMBER $submitter The submitter (null: current member)
 * @param  ?TIME $add_date The time of adding (null: now)
 * @param  ?TIME $edit_date The time of editing (null: never)
 * @param  integer $views The number of views
 * @param  ?AUTO_LINK $id Force an ID (null: don't force an ID)
 * @param  ?SHORT_TEXT $meta_keywords Meta keywords for this resource (null: do not edit) (blank: implicit)
 * @param  ?LONG_TEXT $meta_description Meta description for this resource (null: do not edit) (blank: implicit)
 * @param  array $regions The regions (empty: not region-limited)
 * @return AUTO_LINK The ID of the new entry
 */
function add_image($title, string $cat, $description, string $url, int $validated, int $allow_rating, int $allow_comments, int $allow_trackbacks, string $notes, ?int $submitter = null, ?int $add_date = null, ?int $edit_date = null, int $views = 0, ?int $id = null, ?string $meta_keywords = '', ?string $meta_description = '', array $regions = []) : int
{
    if ((get_param_string('type', null) !== '__import') && (is_string($title))) {
        require_code('global4');
        prevent_double_submit('ADD_IMAGE', null, $title);
    }

    if ($submitter === null) {
        $submitter = get_member();
    }
    if ($add_date === null) {
        $add_date = time();
    }

    if (!addon_installed('validation')) {
        $validated = 1;
    }
    $map = [
        'edit_date' => $edit_date,
        'image_views' => $views,
        'add_date' => $add_date,
        'allow_rating' => $allow_rating,
        'allow_comments' => $allow_comments,
        'allow_trackbacks' => $allow_trackbacks,
        'notes' => $notes,
        'submitter' => $submitter,
        'url' => $url,
        'cat' => $cat,
        'validated' => $validated,
    ];
    if (!is_array($title)) {
        $map += insert_lang('title', $title, 2);
    } else {
        $map += $title;
    }
    global $OVERRIDE_MEMBER_ID_COMCODE;
    $OVERRIDE_MEMBER_ID_COMCODE = $submitter; // Needed for installer, which uses complex HTML
    if (!is_array($description)) {
        $map += insert_lang_comcode('the_description', $description, 3);
    } else {
        $map += $description;
    }
    $OVERRIDE_MEMBER_ID_COMCODE = null;
    if ($id !== null) {
        $map['id'] = $id;
    }
    $id = $GLOBALS['SITE_DB']->query_insert('images', $map, true);

    foreach ($regions as $region) {
        $GLOBALS['SITE_DB']->query_insert('content_regions', ['content_type' => 'image', 'content_id' => strval($id), 'region' => $region]);
    }

    reorganise_uploads__gallery_images(['id' => $id]);

    if (is_array($title)) {
        $title = get_translated_text($title['title']);
    }

    log_it('ADD_IMAGE', strval($id), $title);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('image', strval($id), null, null, true);
    }

    if (!running_script('install')) {
        require_code('content2');
        if (($meta_keywords == '') && ($meta_description == '')) {
            if (!is_array($description)) {
                seo_meta_set_for_implicit('image', strval($id), [$description], $description);
            }
        } else {
            seo_meta_set_for_explicit('image', strval($id), $meta_keywords, $meta_description);
        }
    }

    if ($validated == 1) {
        if (addon_installed('content_privacy')) {
            require_code('content_privacy');
            $privacy_limits = privacy_limits_for('image', strval($id));
        } else {
            $privacy_limits = null;
        }

        require_lang('galleries');
        require_code('notifications');
        $subject = do_lang('IMAGE_NOTIFICATION_MAIL_SUBJECT', get_site_name(), strip_comcode($title));
        $self_url = build_url(['page' => 'galleries', 'type' => 'image', 'id' => $id], get_module_zone('galleries'), [], false, false, true);
        $mail = do_notification_lang('IMAGE_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape($title), [comcode_escape($self_url->evaluate())]);
        dispatch_notification('gallery_entry', $cat, $subject, $mail, $privacy_limits);
    }

    decache_gallery_blocks();

    require_code('member_mentions');
    dispatch_member_mention_notifications('image', strval($id), $submitter);

    require_code('sitemap_xml');
    notify_sitemap_node_add('_SEARCH:galleries:image:' . strval($id), $add_date, $edit_date, SITEMAP_IMPORTANCE_LOW, 'yearly', has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'galleries', $cat));

    return $id;
}

/**
 * Edit an image in a specified gallery.
 *
 * @param  AUTO_LINK $id The ID of the image to edit
 * @param  SHORT_TEXT $title Image title
 * @param  ID_TEXT $cat The gallery name
 * @param  LONG_TEXT $description The image description
 * @param  URLPATH $url The URL to the actual image
 * @param  BINARY $validated Whether the image has been validated for display on the site
 * @param  BINARY $allow_rating Whether the image may be rated
 * @param  SHORT_INTEGER $allow_comments Whether the image may be commented upon
 * @param  BINARY $allow_trackbacks Whether the image may be trackbacked
 * @param  LONG_TEXT $notes Hidden notes associated with the image
 * @param  SHORT_TEXT $meta_keywords Meta keywords
 * @param  LONG_TEXT $meta_description Meta description
 * @param  ?TIME $edit_time Edit time (null: either means current time, or if $null_is_literal, means reset to to null)
 * @param  ?TIME $add_time Add time (null: do not change)
 * @param  ?integer $views Number of views (null: do not change)
 * @param  ?MEMBER $submitter Submitter (null: do not change)
 * @param  array $regions The regions (empty: not region-limited)
 * @param  boolean $null_is_literal Determines whether some nulls passed mean 'use a default' or literally mean 'set to null'
 */
function edit_image(int $id, string $title, string $cat, string $description, string $url, int $validated, int $allow_rating, int $allow_comments, int $allow_trackbacks, string $notes, string $meta_keywords, string $meta_description, ?int $edit_time = null, ?int $add_time = null, ?int $views = null, ?int $submitter = null, array $regions = [], bool $null_is_literal = false)
{
    if ($edit_time === null) {
        $edit_time = $null_is_literal ? null : time();
    }

    $rows = $GLOBALS['SITE_DB']->query_select('images', ['title', 'the_description', 'cat'], ['id' => $id]);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'image'));
    }

    $_title = $rows[0]['title'];
    $_description = $rows[0]['the_description'];
    $old_cat = $rows[0]['cat'];

    require_code('urls2');
    suggest_new_idmoniker_for('galleries', 'image', strval($id), '', ($title == '') ? $description : $title);

    decache_gallery_blocks();

    require_code('files2');
    delete_upload('uploads/galleries', 'images', 'url', 'id', $id, $url);

    if (!addon_installed('validation')) {
        $validated = 1;
    }

    require_code('submit');
    $just_validated = (!content_validated('image', strval($id))) && ($validated == 1);
    if ($just_validated) {
        send_content_validated_notification('image', strval($id));
        $username = $GLOBALS['FORUM_DRIVER']->get_username(get_member());
        log_it('VALIDATE_IMAGE', strval($id), $username);
    }

    $update_map = [
        'allow_rating' => $allow_rating,
        'allow_comments' => $allow_comments,
        'allow_trackbacks' => $allow_trackbacks,
        'notes' => $notes,
        'validated' => $validated,
        'cat' => $cat,
        'url' => $url,
    ];
    $update_map += lang_remap('title', $_title, $title);
    $update_map += lang_remap_comcode('the_description', $_description, $description);

    $update_map['edit_date'] = $edit_time;
    if ($add_time !== null) {
        $update_map['add_date'] = $add_time;
    }
    if ($views !== null) {
        $update_map['image_views'] = $views;
    }
    if ($submitter !== null) {
        $update_map['submitter'] = $submitter;
    }

    $GLOBALS['SITE_DB']->query_update('images', $update_map, ['id' => $id], '', 1);

    $GLOBALS['SITE_DB']->query_delete('content_regions', ['content_type' => 'image', 'content_id' => strval($id)]);
    foreach ($regions as $region) {
        $GLOBALS['SITE_DB']->query_insert('content_regions', ['content_type' => 'image', 'content_id' => strval($id), 'region' => $region]);
    }

    $self_url = build_url(['page' => 'galleries', 'type' => 'image', 'id' => $id], get_module_zone('galleries'), [], false, false, true);

    if ($just_validated) {
        if (addon_installed('content_privacy')) {
            require_code('content_privacy');
            $privacy_limits = privacy_limits_for('image', strval($id));
        } else {
            $privacy_limits = null;
        }

        require_lang('galleries');
        require_code('notifications');
        $subject = do_lang('IMAGE_NOTIFICATION_MAIL_SUBJECT', get_site_name(), strip_comcode($title));
        $mail = do_notification_lang('IMAGE_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape($title), [comcode_escape($self_url->evaluate())]);
        dispatch_notification('gallery_entry', $cat, $subject, $mail, $privacy_limits);
    }

    reorganise_uploads__gallery_images(['id' => $id]);

    log_it('EDIT_IMAGE', strval($id), $title);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('image', strval($id));
    }

    require_code('content2');
    seo_meta_set_for_explicit('image', strval($id), $meta_keywords, $meta_description);

    decache_gallery_blocks();

    require_lang('galleries');
    require_code('feedback');
    update_spacer_post(
        $allow_comments != 0,
        'images',
        strval($id),
        $self_url,
        do_lang('VIEW_IMAGE', '', '', '', get_site_default_lang()),
        process_overridden_comment_forum('images', strval($id), $cat, $old_cat)
    );

    require_code('sitemap_xml');
    notify_sitemap_node_edit('_SEARCH:galleries:image:' . strval($id), has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'galleries', $cat));
}

/**
 * Delete a specified image from the database, and delete the file if possible.
 *
 * @param  AUTO_LINK $id The ID of the image
 * @param  boolean $delete_full Whether to delete the actual file also
 */
function delete_image(int $id, bool $delete_full = true)
{
    $rows = $GLOBALS['SITE_DB']->query_select('images', ['title', 'the_description', 'cat'], ['id' => $id]);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'image'));
    }

    $title = $rows[0]['title'];
    $description = $rows[0]['the_description'];
    $cat = $rows[0]['cat'];

    delete_lang($title);
    delete_lang($description);

    if (addon_installed('catalogues')) {
        update_catalogue_content_ref('image', strval($id), '');
    }

    // Delete file
    if ($delete_full) {
        require_code('files2');
        delete_upload('uploads/galleries', 'images', 'url', 'id', $id);
    }

    // Delete from database
    $GLOBALS['SITE_DB']->query_delete('images', ['id' => $id], '', 1);
    $GLOBALS['SITE_DB']->query_delete('rating', ['rating_for_type' => 'images', 'rating_for_id' => strval($id)]);
    $GLOBALS['SITE_DB']->query_delete('trackbacks', ['trackback_for_type' => 'images', 'trackback_for_id' => strval($id)]);
    $GLOBALS['SITE_DB']->query_delete('content_regions', ['content_type' => 'image', 'content_id' => strval($id)]);
    require_code('notifications');
    delete_all_notifications_on('comment_posted', 'images_' . strval($id));

    require_code('content2');
    seo_meta_erase_storage('image', strval($id));

    decache_gallery_blocks();

    $GLOBALS['SITE_DB']->query_update('url_id_monikers', ['m_deprecated' => 1], ['m_resource_page' => 'galleries', 'm_resource_type' => 'image', 'm_resource_id' => strval($id)]);

    require_code('uploads2');
    clean_empty_upload_directories('uploads/galleries');

    log_it('DELETE_IMAGE', strval($id), get_translated_text($title));

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        expunge_resource_fs_moniker('image', strval($id));
    }

    require_code('sitemap_xml');
    notify_sitemap_node_delete('_SEARCH:galleries:image:' . strval($id));
}

/**
 * Add a video to a specified gallery.
 *
 * @param  SHORT_TEXT $title Video title
 * @param  ID_TEXT $cat The gallery name
 * @param  LONG_TEXT $description The video description
 * @param  URLPATH $url The URL to the actual video
 * @param  URLPATH $thumb_url The URL to the thumbnail of the actual video
 * @param  BINARY $validated Whether the video has been validated for display on the site
 * @param  BINARY $allow_rating Whether the video may be rated
 * @param  SHORT_INTEGER $allow_comments Whether the video may be commented upon
 * @param  BINARY $allow_trackbacks Whether the video may be trackbacked
 * @param  LONG_TEXT $notes Hidden notes associated with the video
 * @param  integer $video_length The length of the video
 * @param  integer $video_width The width of the video
 * @param  integer $video_height The height of the video
 * @param  URLPATH $closed_captions_url The URL to the closed captions file for the video
 * @param  ?MEMBER $submitter The submitter (null: current member)
 * @param  ?TIME $add_date The time of adding (null: now)
 * @param  ?TIME $edit_date The time of editing (null: never)
 * @param  integer $views The number of views
 * @param  ?AUTO_LINK $id Force an ID (null: don't force an ID)
 * @param  ?SHORT_TEXT $meta_keywords Meta keywords for this resource (null: do not edit) (blank: implicit)
 * @param  ?LONG_TEXT $meta_description Meta description for this resource (null: do not edit) (blank: implicit)
 * @param  array $regions The regions (empty: not region-limited)
 * @return AUTO_LINK The ID of the new entry
 */
function add_video(string $title, string $cat, string $description, string $url, string $thumb_url, int $validated, int $allow_rating, int $allow_comments, int $allow_trackbacks, string $notes, int $video_length, int $video_width, int $video_height, string $closed_captions_url = '', ?int $submitter = null, ?int $add_date = null, ?int $edit_date = null, int $views = 0, ?int $id = null, ?string $meta_keywords = '', ?string $meta_description = '', array $regions = []) : int
{
    if (get_param_string('type', null) !== '__import') {
        require_code('global4');
        prevent_double_submit('ADD_VIDEO', null, $title);
    }

    if ($submitter === null) {
        $submitter = get_member();
    }
    if ($add_date === null) {
        $add_date = time();
    }

    if (!addon_installed('validation')) {
        $validated = 1;
    }
    $map = [
        'edit_date' => $edit_date,
        'video_views' => $views,
        'add_date' => $add_date,
        'allow_rating' => $allow_rating,
        'allow_comments' => $allow_comments,
        'allow_trackbacks' => $allow_trackbacks,
        'notes' => $notes,
        'submitter' => $submitter,
        'url' => $url,
        'thumb_url' => $thumb_url,
        'cat' => $cat,
        'validated' => $validated,
        'video_length' => $video_length,
        'video_width' => $video_width,
        'video_height' => $video_height,
        'closed_captions_url' => $closed_captions_url,
    ];
    $map += insert_lang('title', $title, 2);
    $map += insert_lang_comcode('the_description', $description, 3);
    if ($id !== null) {
        $map['id'] = $id;
    }
    $id = $GLOBALS['SITE_DB']->query_insert('videos', $map, true);

    foreach ($regions as $region) {
        $GLOBALS['SITE_DB']->query_insert('content_regions', ['content_type' => 'video', 'content_id' => strval($id), 'region' => $region]);
    }

    $consider_deferring = (!url_is_local($url)) || (filesize(get_custom_file_base() . '/' . rawurldecode($url)) > 1024 * 1024 * 20);

    reorganise_uploads__gallery_videos(['id' => $id]);

    log_it('ADD_VIDEO', strval($id), $title);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('video', strval($id), null, null, true);
    }

    if ($validated == 1) {
        if (addon_installed('content_privacy')) {
            require_code('content_privacy');
            $privacy_limits = privacy_limits_for('video', strval($id));
        } else {
            $privacy_limits = null;
        }

        require_lang('galleries');
        require_code('notifications');
        $subject = do_lang('VIDEO_NOTIFICATION_MAIL_SUBJECT', get_site_name(), strip_comcode($title));
        $self_url = build_url(['page' => 'galleries', 'type' => 'video', 'id' => $id], get_module_zone('galleries'), [], false, false, true);
        $mail = do_notification_lang('VIDEO_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape($title), [comcode_escape($self_url->evaluate())]);
        dispatch_notification('gallery_entry', $cat, $subject, $mail, $privacy_limits);
    }

    require_code('content2');
    if (($meta_keywords == '') && ($meta_description == '')) {
        seo_meta_set_for_implicit('video', strval($id), [$description], $description);
    } else {
        seo_meta_set_for_explicit('video', strval($id), $meta_keywords, $meta_description);
    }

    decache_gallery_blocks();

    require_code('member_mentions');
    dispatch_member_mention_notifications('video', strval($id), $submitter);

    require_code('sitemap_xml');
    notify_sitemap_node_add('_SEARCH:galleries:video:' . strval($id), $add_date, $edit_date, SITEMAP_IMPORTANCE_HIGH, 'yearly', has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'galleries', $cat));

    return $id;
}

/**
 * Edit a video in a specified gallery.
 *
 * @param  AUTO_LINK $id The ID of the entry to edit
 * @param  SHORT_TEXT $title Video title
 * @param  ID_TEXT $cat The gallery name
 * @param  LONG_TEXT $description The video description
 * @param  URLPATH $url The URL to the actual video
 * @param  URLPATH $thumb_url The URL to the thumbnail of the actual video
 * @param  BINARY $validated Whether the video has been validated for display on the site
 * @param  BINARY $allow_rating Whether the video may be rated
 * @param  SHORT_INTEGER $allow_comments Whether the video may be commented upon
 * @param  BINARY $allow_trackbacks Whether the video may be trackbacked
 * @param  LONG_TEXT $notes Hidden notes associated with the video
 * @param  integer $video_length The length of the video
 * @param  integer $video_width The width of the video
 * @param  integer $video_height The height of the video
 * @param  SHORT_TEXT $meta_keywords Meta keywords
 * @param  LONG_TEXT $meta_description Meta description
 * @param  ?URLPATH $closed_captions_url The URL to the closed captions for this video (null: do not change)
 * @param  ?TIME $edit_time Edit time (null: either means current time, or if $null_is_literal, means reset to to null)
 * @param  ?TIME $add_time Add time (null: do not change)
 * @param  ?integer $views Number of views (null: do not change)
 * @param  ?MEMBER $submitter Submitter (null: do not change)
 * @param  array $regions The regions (empty: not region-limited)
 * @param  boolean $null_is_literal Determines whether some nulls passed mean 'use a default' or literally mean 'set to null'
 */
function edit_video(int $id, string $title, string $cat, string $description, string $url, string $thumb_url, int $validated, int $allow_rating, int $allow_comments, int $allow_trackbacks, string $notes, int $video_length, int $video_width, int $video_height, string $meta_keywords, string $meta_description, ?string $closed_captions_url = null, ?int $edit_time = null, ?int $add_time = null, ?int $views = null, ?int $submitter = null, array $regions = [], bool $null_is_literal = false)
{
    if ($edit_time === null) {
        $edit_time = $null_is_literal ? null : time();
    }

    $rows = $GLOBALS['SITE_DB']->query_select('videos', ['title', 'the_description', 'cat', 'url'], ['id' => $id]);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'video'));
    }

    $_title = $rows[0]['title'];
    $_description = $rows[0]['the_description'];
    $old_cat = $rows[0]['cat'];
    $orig_url = $rows[0]['url'];

    require_code('urls2');
    suggest_new_idmoniker_for('galleries', 'video', strval($id), '', ($title == '') ? $description : $title);

    require_code('files2');
    delete_upload('uploads/galleries', 'videos', 'url', 'id', $id, $url);
    delete_upload('uploads/galleries_thumbs', 'videos', 'thumb_url', 'id', $id, $thumb_url);

    if (!addon_installed('validation')) {
        $validated = 1;
    }

    require_code('submit');
    $just_validated = (!content_validated('video', strval($id))) && ($validated == 1);
    if ($just_validated) {
        send_content_validated_notification('video', strval($id));
        $username = $GLOBALS['FORUM_DRIVER']->get_username(get_member());
        log_it('VALIDATE_VIDEO', strval($id), $username);
    }

    $update_map = [
        'allow_rating' => $allow_rating,
        'allow_comments' => $allow_comments,
        'allow_trackbacks' => $allow_trackbacks,
        'notes' => $notes,
        'validated' => $validated,
        'cat' => $cat,
        'url' => $url,
        'thumb_url' => $thumb_url,
        'video_length' => $video_length,
        'video_width' => $video_width,
        'video_height' => $video_height,
    ];
    $update_map += lang_remap('title', $_title, $title);
    $update_map += lang_remap_comcode('the_description', $_description, $description);

    $update_map['edit_date'] = $edit_time;
    if ($closed_captions_url !== null) {
        $update_map['closed_captions_url'] = $closed_captions_url;
    }
    if ($add_time !== null) {
        $update_map['add_date'] = $add_time;
    }
    if ($views !== null) {
        $update_map['video_views'] = $views;
    }
    if ($submitter !== null) {
        $update_map['submitter'] = $submitter;
    }

    $GLOBALS['SITE_DB']->query_update('videos', $update_map, ['id' => $id], '', 1);

    $GLOBALS['SITE_DB']->query_delete('content_regions', ['content_type' => 'video', 'content_id' => strval($id)]);
    foreach ($regions as $region) {
        $GLOBALS['SITE_DB']->query_insert('content_regions', ['content_type' => 'video', 'content_id' => strval($id), 'region' => $region]);
    }

    $self_url = build_url(['page' => 'galleries', 'type' => 'video', 'id' => $id], get_module_zone('galleries'), [], false, false, true);

    if ($just_validated) {
        if (addon_installed('content_privacy')) {
            require_code('content_privacy');
            $privacy_limits = privacy_limits_for('video', strval($id));
        } else {
            $privacy_limits = null;
        }

        require_lang('galleries');
        require_code('notifications');
        $subject = do_lang('VIDEO_NOTIFICATION_MAIL_SUBJECT', get_site_name(), strip_comcode($title));
        $mail = do_notification_lang('VIDEO_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape($title), [comcode_escape($self_url->evaluate())]);
        dispatch_notification('gallery_entry', $cat, $subject, $mail, $privacy_limits);
    }

    reorganise_uploads__gallery_videos(['id' => $id]);

    log_it('EDIT_VIDEO', strval($id), $title);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('video', strval($id));
    }

    require_code('content2');
    seo_meta_set_for_explicit('video', strval($id), $meta_keywords, $meta_description);

    decache_gallery_blocks();

    require_lang('galleries');
    require_code('feedback');
    update_spacer_post(
        $allow_comments != 0,
        'videos',
        strval($id),
        $self_url,
        do_lang('VIEW_VIDEO', '', '', '', get_site_default_lang()),
        process_overridden_comment_forum('videos', strval($id), $cat, $old_cat)
    );

    require_code('sitemap_xml');
    notify_sitemap_node_edit('_SEARCH:galleries:video:' . strval($id), has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'galleries', $cat));
}

/**
 * Delete a video in a specified gallery.
 *
 * @param  AUTO_LINK $id The ID of the entry to delete
 * @param  boolean $delete_full Whether to delete the actual video file from disk as well as the entry
 */
function delete_video(int $id, bool $delete_full = true)
{
    $rows = $GLOBALS['SITE_DB']->query_select('videos', ['title', 'the_description', 'cat'], ['id' => $id], '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'video'));
    }
    $title = $rows[0]['title'];
    $description = $rows[0]['the_description'];
    $cat = $rows[0]['cat'];

    delete_lang($title);
    delete_lang($description);

    if (addon_installed('catalogues')) {
        update_catalogue_content_ref('video', strval($id), '');
    }

    if ($delete_full) {
        require_code('files2');
        delete_upload('uploads/galleries', 'videos', 'url', 'id', $id);
        delete_upload('uploads/galleries', 'videos', 'closed_captions_url', 'id', $id);
        delete_upload('uploads/galleries_thumbs', 'videos', 'thumb_url', 'id', $id);
    }

    // Delete from database
    $GLOBALS['SITE_DB']->query_delete('videos', ['id' => $id], '', 1);
    $GLOBALS['SITE_DB']->query_delete('rating', ['rating_for_type' => 'videos', 'rating_for_id' => strval($id)]);
    $GLOBALS['SITE_DB']->query_delete('trackbacks', ['trackback_for_type' => 'videos', 'trackback_for_id' => strval($id)]);
    $GLOBALS['SITE_DB']->query_delete('content_regions', ['content_type' => 'video', 'content_id' => strval($id)]);
    require_code('notifications');
    delete_all_notifications_on('comment_posted', 'videos_' . strval($id));

    require_code('content2');
    seo_meta_erase_storage('video', strval($id));

    decache_gallery_blocks();

    $GLOBALS['SITE_DB']->query_update('url_id_monikers', ['m_deprecated' => 1], ['m_resource_page' => 'galleries', 'm_resource_type' => 'video', 'm_resource_id' => strval($id)]);

    require_code('uploads2');
    clean_empty_upload_directories('uploads/galleries');
    clean_empty_upload_directories('uploads/galleries_thumbs');

    log_it('DELETE_VIDEO', strval($id), get_translated_text($title));

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        expunge_resource_fs_moniker('video', strval($id));
    }

    require_code('sitemap_xml');
    notify_sitemap_node_delete('_SEARCH:galleries:video:' . strval($id));
}

/**
 * Find how to apply gallery watermarks.
 *
 * @param  ID_TEXT $gallery The name of the gallery for the image
 * @return ?array A quartet of watermark images suitable for handle_images_cleanup_pipeline (null: no watermark images)
 */
function find_gallery_watermarks(string $gallery) : ?array
{
    // We need to find the most applicable gallery watermarks
    $watermark_top_left = '';
    $watermark_top_right = '';
    $watermark_bottom_left = '';
    $watermark_bottom_right = '';
    do {
        if ($gallery == '') {
            return null; // We couldn't find any matermarks
        }

        $_gallery = $GLOBALS['SITE_DB']->query_select('galleries', ['parent_id', 'watermark_top_left', 'watermark_top_right', 'watermark_bottom_left', 'watermark_bottom_right'], ['name' => $gallery], '', 1);
        $watermark_top_left = $_gallery[0]['watermark_top_left'];
        $watermark_top_right = $_gallery[0]['watermark_top_right'];
        $watermark_bottom_left = $_gallery[0]['watermark_bottom_left'];
        $watermark_bottom_right = $_gallery[0]['watermark_bottom_right'];
        $gallery = $_gallery[0]['parent_id'];
    } while (($watermark_top_left == '') && ($watermark_top_right == '') && ($watermark_bottom_left == '') && ($watermark_bottom_right == ''));

    if ($watermark_top_left . $watermark_top_right . $watermark_bottom_left . $watermark_bottom_right == '') {
        return null;
    }

    return [$watermark_top_left, $watermark_top_right, $watermark_bottom_left, $watermark_bottom_right];
}

/**
 * Watermark the corner of an image.
 *
 * @param  resource $source The image resource being watermarked
 * @param  URLPATH $watermark_url The URL to the watermark file
 * @param  BINARY $x Whether a right hand side corner is being watermarked
 * @param  BINARY $y Whether a bottom edge corner is being watermarked
 *
 * @ignore
 */
function _watermark_corner($source, string $watermark_url, int $x, int $y)
{
    if ($watermark_url != '') {
        $watermark_path = get_custom_file_base() . '/' . rawurldecode($watermark_url);
        $watermark = cms_imagecreatefrom($watermark_path);
        if ($watermark !== false) {
            imagecolortransparent($watermark, imagecolorallocate($watermark, 255, 0, 255));
            if ($x == 1) {
                $x = imagesx($source) - imagesx($watermark);
            }
            if ($y == 1) {
                $y = imagesy($source) - imagesy($watermark);
            }
            imagecopy($source, $watermark, $x, $y, 0, 0, imagesx($watermark), imagesy($watermark));
            imagedestroy($watermark);
        }
    }
}

/**
 * Add a gallery with the specified parameters.
 *
 * @param  ID_TEXT $name The gallery codename
 * @param  SHORT_TEXT $fullname The full human-readable name of the gallery
 * @param  LONG_TEXT $description The description of the gallery
 * @param  LONG_TEXT $notes Hidden notes associated with the gallery
 * @param  ID_TEXT $parent_id The parent gallery (blank: no parent)
 * @param  BINARY $accept_images Whether images may be put in this gallery
 * @param  BINARY $accept_videos Whether videos may be put in this gallery
 * @param  BINARY $is_member_synched Whether the gallery serves as a container for automatically created member galleries
 * @param  ?ID_TEXT $layout_mode The layout mode this gallery uses (null: use default mode)
 * @param  URLPATH $rep_image The representative image of the gallery (blank: none)
 * @param  URLPATH $watermark_top_left Watermark (blank: none)
 * @param  URLPATH $watermark_top_right Watermark (blank: none)
 * @param  URLPATH $watermark_bottom_left Watermark (blank: none)
 * @param  URLPATH $watermark_bottom_right Watermark (blank: none)
 * @param  SHORT_TEXT $gallery_sort The sub-gallery sort order (blank: site default)
 * @param  SHORT_TEXT $media_sort The sort order of media in the gallery (blank: site default)
 * @param  BINARY $allow_rating Whether rating are allowed
 * @param  SHORT_INTEGER $allow_comments Whether comments are allowed
 * @param  boolean $skip_exists_check Whether to skip the check for whether the gallery exists (useful for importers)
 * @param  ?TIME $add_date The add time (null: now)
 * @param  ?MEMBER $g_owner The gallery owner (null: nobody)
 * @param  ?SHORT_TEXT $meta_keywords Meta keywords for this resource (null: do not edit) (blank: implicit)
 * @param  ?LONG_TEXT $meta_description Meta description for this resource (null: do not edit) (blank: implicit)
 * @param  boolean $uniqify Whether to force the name as unique, if there's a conflict
 * @return ID_TEXT The name
 */
function add_gallery(string $name, string $fullname, string $description, string $notes, string $parent_id, int $accept_images = 1, int $accept_videos = 1, int $is_member_synched = 0, ?string $layout_mode = null, string $rep_image = '', string $watermark_top_left = '', string $watermark_top_right = '', string $watermark_bottom_left = '', string $watermark_bottom_right = '', string $gallery_sort = '', string $media_sort = '', int $allow_rating = 0, int $allow_comments = 0, bool $skip_exists_check = false, ?int $add_date = null, ?int $g_owner = null, ?string $meta_keywords = '', ?string $meta_description = '', bool $uniqify = false) : string
{
    require_code('galleries');

    if ($layout_mode === null) {
        $layout_mode = GALLERY_LAYOUT_MODE_DEFAULT;
    }

    if ($add_date === null) {
        $add_date = time();
    }

    require_code('type_sanitisation');
    if (!is_alphanumeric($name)) {
        warn_exit(do_lang_tempcode('BAD_CODENAME'));
    }

    if (!$skip_exists_check) {
        $test = $GLOBALS['SITE_DB']->query_select_value_if_there('galleries', 'name', ['name' => $name]);
        if ($test !== null) {
            if ($uniqify) {
                $name .= '_' . uniqid('', false);
            } else {
                warn_exit(do_lang_tempcode('ALREADY_EXISTS', escape_html($name)));
            }
        }
    }

    $map = [
        'name' => $name,
        'add_date' => $add_date,
        'notes' => $notes,
        'watermark_top_left' => $watermark_top_left,
        'watermark_top_right' => $watermark_top_right,
        'watermark_bottom_left' => $watermark_bottom_left,
        'watermark_bottom_right' => $watermark_bottom_right,
        'parent_id' => $parent_id,
        'accept_images' => $accept_images,
        'rep_image' => $rep_image,
        'accept_videos' => $accept_videos,
        'is_member_synched' => $is_member_synched,
        'layout_mode' => $layout_mode,
        'allow_rating' => $allow_rating,
        'allow_comments' => $allow_comments,
        'g_owner' => $g_owner,
        'gallery_views' => 0,
        'gallery_sort' => $gallery_sort,
        'media_sort' => $media_sort,
    ];
    $map += insert_lang_comcode('the_description', $description, 2);
    $map += insert_lang_comcode('fullname', $fullname, 1);
    $GLOBALS['SITE_DB']->query_insert('galleries', $map);

    reorganise_uploads__galleries(['name' => $name]);

    log_it('ADD_GALLERY', $name, $fullname);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('gallery', $name, null, null, true);
    }

    if ($parent_id != '') {
        require_code('notifications2');
        copy_notifications_to_new_child('gallery_entry', $parent_id, $name);
    }

    require_code('content2');
    seo_meta_set_for_implicit('gallery', $name, [$fullname, $description], $description);

    if (($meta_keywords == '') && ($meta_description == '')) {
        seo_meta_set_for_implicit('gallery', $name, [$description], $description);
    } else {
        seo_meta_set_for_explicit('gallery', $name, $meta_keywords, $meta_description);
    }

    decache_gallery_blocks();

    require_code('member_mentions');
    dispatch_member_mention_notifications('gallery', $name, $g_owner);

    require_code('sitemap_xml');
    notify_sitemap_node_add('_SEARCH:galleries:browse:' . $name, $add_date, null, SITEMAP_IMPORTANCE_MEDIUM, 'monthly', has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'galleries', $name));

    return $name;
}

/**
 * Edit a gallery.
 *
 * @param  ID_TEXT $old_name The old gallery codename (in case we are renaming)
 * @param  ID_TEXT $name The gallery codename (maybe the same as the old one)
 * @param  SHORT_TEXT $fullname The full human-readable name of the gallery
 * @param  LONG_TEXT $description The description of the gallery
 * @param  LONG_TEXT $notes Hidden notes associated with the gallery
 * @param  ?ID_TEXT $parent_id The parent gallery (null: no parent)
 * @param  BINARY $accept_images Whether images may be put in this gallery
 * @param  BINARY $accept_videos Whether videos may be put in this gallery
 * @param  BINARY $is_member_synched Whether the gallery serves as a container for automatically created member galleries
 * @param  ?ID_TEXT $layout_mode The layout mode this gallery uses (null: preserve existing mode)
 * @param  ?URLPATH $rep_image The representative image of the gallery (null: none)
 * @param  ?URLPATH $watermark_top_left Watermark (null: none)
 * @param  ?URLPATH $watermark_top_right Watermark (null: none)
 * @param  ?URLPATH $watermark_bottom_left Watermark (null: none)
 * @param  ?URLPATH $watermark_bottom_right Watermark (null: none)
 * @param  ?SHORT_TEXT $gallery_sort The sort order for sub-galleries (null: do not edit)
 * @param  ?SHORT_TEXT $media_sort The sort order for media within the gallery (null: do not edit)
 * @param  ?SHORT_TEXT $meta_keywords Meta keywords for this resource (null: do not edit)
 * @param  ?LONG_TEXT $meta_description Meta description for this resource (null: do not edit)
 * @param  BINARY $allow_rating Whether rating are allowed
 * @param  SHORT_INTEGER $allow_comments Whether comments are allowed
 * @param  ?MEMBER $g_owner The gallery owner (null: nobody)
 * @param  ?TIME $add_time The add time (null: now)
 * @param  boolean $null_is_literal Determines whether some nulls passed mean 'use a default' or literally mean 'set to null'
 * @param  boolean $uniqify Whether to force the name as unique, if there's a conflict
 * @return ID_TEXT The name
 */
function edit_gallery(string $old_name, string $name, string $fullname, string $description, string $notes, ?string $parent_id = null, int $accept_images = 1, int $accept_videos = 1, int $is_member_synched = 0, ?string $layout_mode = null, ?string $rep_image = null, ?string $watermark_top_left = null, ?string $watermark_top_right = null, ?string $watermark_bottom_left = null, ?string $watermark_bottom_right = null, ?string $gallery_sort = null, ?string $media_sort = null, ?string $meta_keywords = null, ?string $meta_description = null, int $allow_rating = 1, int $allow_comments = 1, ?int $g_owner = null, ?int $add_time = null, bool $null_is_literal = false, bool $uniqify = false) : string
{
    $rows = $GLOBALS['SITE_DB']->query_select('galleries', ['*'], ['name' => $old_name], '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'gallery'));
    }

    require_code('urls2');
    suggest_new_idmoniker_for('galleries', 'browse', $name, '', $name);

    $under_category_id = $parent_id;
    while (($under_category_id != '') && ($under_category_id != STRING_MAGIC_NULL)) {
        if ($name == $under_category_id) {
            warn_exit(do_lang_tempcode('OWN_PARENT_ERROR', 'gallery'));
        }
        $_under_category_id = $GLOBALS['SITE_DB']->query_select_value('galleries', 'parent_id', ['name' => $under_category_id]);
        if ($under_category_id == $_under_category_id) {
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
        }
        $under_category_id = $_under_category_id;
    }

    if ($parent_id === null) {
        $parent_id = '';
    }

    require_code('content2');

    if ($old_name != $name) {
        require_code('type_sanitisation');
        if (!is_alphanumeric($name)) {
            warn_exit(do_lang_tempcode('BAD_CODENAME'));
        }

        $test = $GLOBALS['SITE_DB']->query_select_value_if_there('galleries', 'name', ['name' => $name]);
        if ($test !== null) {
            if ($uniqify) {
                $name .= '_' . uniqid('', false);
            } else {
                warn_exit(do_lang_tempcode('ALREADY_EXISTS', escape_html($name)));
            }
        }

        seo_meta_erase_storage('gallery', $old_name);
        $GLOBALS['SITE_DB']->query_update('images', ['cat' => $name], ['cat' => $old_name]);
        $GLOBALS['SITE_DB']->query_update('videos', ['cat' => $name], ['cat' => $old_name]);
        $GLOBALS['SITE_DB']->query_update('galleries', ['parent_id' => $name], ['parent_id' => $old_name]);
        if (addon_installed('awards')) {
            $types = $GLOBALS['SITE_DB']->query_select('award_types', ['id'], ['a_content_type' => 'gallery']);
            foreach ($types as $type) {
                $GLOBALS['SITE_DB']->query_update('award_archive', ['content_id' => $name], ['content_id' => $old_name, 'a_type_id' => $type['id']]);
            }
        }

        if (addon_installed('catalogues')) {
            update_catalogue_content_ref('gallery', $old_name, $name);
        }

        require_code('sitemap_xml');
        notify_sitemap_node_delete('_SEARCH:galleries:browse:' . $old_name);
    }

    if ($meta_keywords !== null) {
        seo_meta_set_for_explicit('gallery', $name, $meta_keywords, $meta_description);
    }

    $rows = $GLOBALS['SITE_DB']->query_select('galleries', ['fullname', 'the_description'], ['name' => $old_name], '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'gallery'));
    }
    $myrow = $rows[0];

    $update_map = [
        'name' => $name,
        'notes' => $notes,
        'parent_id' => $parent_id,
        'accept_images' => $accept_images,
        'accept_videos' => $accept_videos,
        'is_member_synched' => $is_member_synched,
        'allow_rating' => $allow_rating,
        'allow_comments' => $allow_comments,
    ];

    if ($layout_mode !== null) {
        $update_map['layout_mode'] = $layout_mode;
    }
    $update_map += lang_remap_comcode('fullname', $myrow['fullname'], $fullname);
    $update_map += lang_remap_comcode('the_description', $myrow['the_description'], $description);

    require_code('files2');

    if ($rep_image !== null) {
        $update_map['rep_image'] = $rep_image;
        delete_upload('uploads/repimages', 'galleries', 'rep_image', 'name', $old_name, $rep_image);
    }
    if ($watermark_top_left !== null) {
        $update_map['watermark_top_left'] = $watermark_top_left;
        delete_upload('uploads/watermarks', 'galleries', 'watermark_top_left', 'name', $old_name, $watermark_top_left);
    }
    if ($watermark_top_right !== null) {
        $update_map['watermark_top_right'] = $watermark_top_right;
        delete_upload('uploads/watermarks', 'galleries', 'watermark_top_right', 'name', $old_name, $watermark_top_right);
    }
    if ($watermark_bottom_left !== null) {
        $update_map['watermark_bottom_left'] = $watermark_bottom_left;
        delete_upload('uploads/watermarks', 'galleries', 'watermark_bottom_left', 'name', $old_name, $watermark_bottom_left);
    }
    if ($watermark_bottom_right !== null) {
        $update_map['watermark_bottom_right'] = $watermark_bottom_right;
        delete_upload('uploads/watermarks', 'galleries', 'watermark_bottom_right', 'name', $old_name, $watermark_bottom_right);
    }

    if ($add_time !== null) {
        $update_map['add_date'] = $add_time;
    }
    if ($g_owner !== null) {
        $update_map['g_owner'] = $g_owner;
    }

    if ($gallery_sort !== null) {
        $update_map['gallery_sort'] = $gallery_sort;
    }
    if ($media_sort !== null) {
        $update_map['media_sort'] = $media_sort;
    }

    $GLOBALS['SITE_DB']->query_update('galleries', $update_map, ['name' => $old_name], '', 1);

    reorganise_uploads__galleries(['name' => $name]);

    log_it('EDIT_GALLERY', $name, $fullname);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('gallery', $name);
    }

    $GLOBALS['SITE_DB']->query_update('group_category_access', ['category_name' => $name], ['module_the_name' => 'galleries', 'category_name' => $old_name]);

    decache_gallery_blocks();

    require_code('feedback');
    update_spacer_post(
        $allow_comments != 0,
        'galleries',
        $name,
        build_url(['page' => 'galleries', 'type' => 'browse', 'id' => $name], get_module_zone('galleries'), [], false, false, true),
        $fullname,
        process_overridden_comment_forum('galleries', $name, $name, $old_name)
    );

    require_code('sitemap_xml');
    notify_sitemap_node_edit('_SEARCH:galleries:browse:' . $name, has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'galleries', $name));

    return $name;
}

/**
 * Delete a specified gallery.
 *
 * @param  ID_TEXT $name The gallery codename
 */
function delete_gallery(string $name)
{
    if ($name == '') {
        warn_exit(do_lang_tempcode('NO_DELETE_ROOT', 'gallery'));
    }

    $rows = $GLOBALS['SITE_DB']->query_select('galleries', ['*'], ['name' => $name], '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'gallery'));
    }

    require_code('files2');
    delete_upload('uploads/repimages', 'galleries', 'rep_image', 'name', $name);
    delete_upload('uploads/watermarks', 'galleries', 'watermark_top_left', 'name', $name);
    delete_upload('uploads/watermarks', 'galleries', 'watermark_top_right', 'name', $name);
    delete_upload('uploads/watermarks', 'galleries', 'watermark_bottom_left', 'name', $name);
    delete_upload('uploads/watermarks', 'galleries', 'watermark_bottom_right', 'name', $name);

    if (addon_installed('catalogues')) {
        update_catalogue_content_ref('gallery', $name, '');
    }

    delete_lang($rows[0]['fullname']);
    delete_lang($rows[0]['the_description']);

    // Images and videos are deleted, because we are deleting the _gallery_, not just a category (nobody is going to be deleting galleries with the expectation of moving the image to a different one in bulk - unlike download categories, for example).
    do {
        cms_extend_time_limit(TIME_LIMIT_EXTEND__MODEST);

        send_http_output_ping();

        $images = $GLOBALS['SITE_DB']->query_select('images', ['id'], ['cat' => $name], '', 200);
        foreach ($images as $image) {
            delete_image($image['id'], false);
        }
    } while (!empty($images));
    do {
        cms_extend_time_limit(TIME_LIMIT_EXTEND__MODEST);

        send_http_output_ping();

        $videos = $GLOBALS['SITE_DB']->query_select('videos', ['id'], ['cat' => $name], '', 200);
        foreach ($videos as $video) {
            delete_video($video['id'], false);
        }
    } while (!empty($videos));
    //... but the subgalleries remain
    $GLOBALS['SITE_DB']->query_update('galleries', ['parent_id' => $rows[0]['parent_id']], ['parent_id' => $name]);

    $GLOBALS['SITE_DB']->query_delete('galleries', ['name' => $name], '', 1);

    $GLOBALS['SITE_DB']->query_delete('rating', ['rating_for_type' => 'images', 'rating_for_id' => $name]);
    $GLOBALS['SITE_DB']->query_delete('rating', ['rating_for_type' => 'videos', 'rating_for_id' => $name]);

    require_code('content2');
    seo_meta_erase_storage('gallery', $name);

    $GLOBALS['SITE_DB']->query_delete('group_category_access', ['module_the_name' => 'galleries', 'category_name' => $name]);
    $GLOBALS['SITE_DB']->query_delete('group_privileges', ['module_the_name' => 'galleries', 'category_name' => $name]);

    decache_gallery_blocks();

    $GLOBALS['SITE_DB']->query_update('url_id_monikers', ['m_deprecated' => 1], ['m_resource_page' => 'galleries', 'm_resource_type' => 'browse', 'm_resource_id' => $name]);

    require_code('uploads2');
    clean_empty_upload_directories('uploads/repimages');
    clean_empty_upload_directories('uploads/watermarks');

    log_it('DELETE_GALLERY', $name, get_translated_text($rows[0]['fullname']));

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        expunge_resource_fs_moniker('gallery', $name);
    }

    require_code('sitemap_xml');
    notify_sitemap_node_delete('_SEARCH:galleries:browse:' . $name);

    if (addon_installed('ecommerce')) {
        require_code('ecommerce_permission_products');
        delete_prod_permission('gallery', $name);
    }
}

/**
 * The UI shows member galleries that do not exist. If it is a member gallery, and it does not exist, it'll need making, before something can be added. This gallery performs the check and makes the gallery if needed.
 *
 * @param  ID_TEXT $cat The gallery codename
 */
function make_member_gallery_if_needed(string $cat)
{
    // If it is a non-member gallery, it must surely exist, as we have no interface to choose non-existent ones (it's safe enough to assume it hasn't been deleted suddenly)

    if (substr($cat, 0, 7) != 'member_') {
        return;
    }

    // Test to see if it exists
    $test = $GLOBALS['SITE_DB']->query_select_value_if_there('galleries', 'name', ['name' => $cat]);
    if ($test === null) {
        $parts = explode('_', $cat, 3);
        $member_id = intval($parts[1]);
        $parent_id = $parts[2];
        if (!has_privilege($member_id, 'have_personal_category', 'cms_galleries')) {
            return;
        }
        $_parent_info = $GLOBALS['SITE_DB']->query_select('galleries', ['accept_images', 'accept_videos', 'layout_mode', 'fullname'], ['name' => $parent_id], '', 1);
        if (!array_key_exists(0, $_parent_info)) {
            fatal_exit(do_lang_tempcode('INTERNAL_ERROR'));
        }
        $parent_info = $_parent_info[0];

        $member_gallery_title = get_potential_gallery_title($cat);
        add_gallery($cat, $member_gallery_title, '', '', $parent_id, $parent_info['accept_images'], $parent_info['accept_videos'], 0, $parent_info['layout_mode']);

        $rows = $GLOBALS['SITE_DB']->query_select('group_category_access', ['group_id'], ['module_the_name' => 'galleries', 'category_name' => $parent_id]);
        foreach ($rows as $row) {
            $GLOBALS['SITE_DB']->query_insert('group_category_access', ['module_the_name' => 'galleries', 'category_name' => $cat, 'group_id' => $row['group_id']]);
        }
    }
}

/**
 * Get the potential title of a gallery - real name if gallery exists.
 *
 * @param  ID_TEXT $cat The gallery codename
 * @return ?SHORT_TEXT The gallery title (null: does not exist and won't be auto-created)
 */
function get_potential_gallery_title(string $cat) : ?string
{
    // Test to see if it exists
    $test = $GLOBALS['SITE_DB']->query_select_value_if_there('galleries', 'fullname', ['name' => $cat]);
    if (($test === null) && (substr($cat, 0, 7) == 'member_')) {
        // Does not exist but is a potential member gallery
        $parts = explode('_', $cat, 3);
        $member_id = intval($parts[1]); // Almost certainly going to be same as get_member(), but we might as well be general here

        // Find about parent (new gallery inherits)
        $parent_id = $parts[2];
        $_parent_info = $GLOBALS['SITE_DB']->query_select('galleries', ['accept_images', 'accept_videos', 'layout_mode', 'fullname'], ['name' => $parent_id], '', 1);
        if (!array_key_exists(0, $_parent_info)) {
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
        }
        $parent_info = $_parent_info[0];

        // Work out name
        $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id, true, USERNAME_DEFAULT_ERROR);
        $fullname = get_translated_text($parent_info['fullname']);
        if ($fullname == do_lang('GALLERIES_HOME')) {
            $fullname = do_lang('GALLERY');
        }
        return do_lang('PERSONAL_GALLERY_OF', $username, $fullname);
    }

    // Does exist
    return get_translated_text($test);
}

/**
 * Reorganise the gallery uploads.
 *
 * @param  array $where Limit reorganisation to rows matching this WHERE map
 * @param  boolean $tolerate_errors Whether to tolerate missing files (false = give an error)
 */
function reorganise_uploads__galleries(array $where = [], bool $tolerate_errors = false)
{
    require_code('uploads2');
    reorganise_uploads('gallery', 'uploads/repimages', 'rep_image', $where, true, $tolerate_errors);
    reorganise_uploads('gallery', 'uploads/watermarks', 'watermark_top_left', $where, false, $tolerate_errors);
    reorganise_uploads('gallery', 'uploads/watermarks', 'watermark_top_right', $where, false, $tolerate_errors);
    reorganise_uploads('gallery', 'uploads/watermarks', 'watermark_bottom_left', $where, false, $tolerate_errors);
    reorganise_uploads('gallery', 'uploads/watermarks', 'watermark_bottom_right', $where, false, $tolerate_errors);
}
/**
 * Reorganise the gallery image uploads.
 *
 * @param  array $where Limit reorganisation to rows matching this WHERE map
 * @param  boolean $tolerate_errors Whether to tolerate missing files (false = give an error)
 */
function reorganise_uploads__gallery_images(array $where = [], bool $tolerate_errors = false)
{
    require_code('uploads2');
    reorganise_uploads('image', 'uploads/galleries', 'url', $where, false, $tolerate_errors);
}
/**
 * Reorganise the gallery video uploads.
 *
 * @param  array $where Limit reorganisation to rows matching this WHERE map
 * @param  boolean $tolerate_errors Whether to tolerate missing files (false = give an error)
 */
function reorganise_uploads__gallery_videos(array $where = [], bool $tolerate_errors = false)
{
    require_code('uploads2');
    reorganise_uploads('video', 'uploads/galleries', 'url', $where, false, $tolerate_errors);
    reorganise_uploads('video', 'uploads/galleries', 'closed_captions_url', $where, false, $tolerate_errors);
    reorganise_uploads('video', 'uploads/galleries_thumbs', 'thumb_url', $where, false, $tolerate_errors);
}

/**
 * Take a file in the gallery uploads directory, and add it to a gallery.
 * Intended for bulk operations, where many mixed media files are imported with common settings and other settings derived from metadata/defaults.
 *
 * @param  URLPATH $url The URL to the file (must be a URL relative to the base URL)
 * @param  ID_TEXT $_cat The gallery to add to
 * @param  MEMBER $member_id The ID of the member adding gallery media
 * @param  integer $allow_rating Post param indicating whether or not ratings should be allowed
 * @param  integer $allow_comments_reviews Post param combination indicating whether to allow comments or reviews
 * @param  integer $allow_trackbacks Post param indicating whether or not to allow trackbacks
 * @param  boolean $watermark Whether or not to apply the gallery's watermarks to the file if it is an image
 * @param  string $notes Staff notes provided for this entry
 * @param  ID_TEXT $privacy_level Level of privacy set for this content
 * @param  array $additional_access Array of additional members who should have access to this content
 * @param  ?string $filename The filename (null: derive from $url)
 * @param  ?TIME $time Timestamp to use (null: now)
 * @param  string $fallback_title The fallback title to use if a title is required but we could not get one (blank: none)
 * @return ?array A pair: The media type, The media ID (null: error)
 */
function add_gallery_media_wrap(string $url, string $_cat, int $member_id, int $allow_rating, int $allow_comments_reviews, int $allow_trackbacks, bool $watermark, string $notes, string $privacy_level, array $additional_access, ?string $filename = null, ?int $time = null, string $fallback_title = '') : ?array
{
    require_code('exif');

    if ($filename === null) {
        $filename = rawurldecode(basename($url));
    }
    if (empty($filename)) {
        $filename = null;
    }

    if ($time === null) {
        $time = time();
    }

    if (!is_image($url, IMAGE_CRITERIA_WEBSAFE, has_privilege($member_id, 'comcode_dangerous'))) {
        // Video...

        list(
            $width,
            $height,
            $length,
            $title,
            $cat,
        ) = video_get_default_metadata($url, '', $filename, null, null, null, '', $_cat);

        if (($title == '') && (get_option('gallery_media_title_required') == '2')) {
            if ($filename !== null) {
                $title = pathinfo($filename, PATHINFO_FILENAME);
            } else {
                $title = $fallback_title;
            }
        }

        $thumb_url = video_get_default_thumb_url($url, '', $filename);

        require_code('images');
        $closed_captions_url = get_matching_closed_captions_file($url);
        if ($closed_captions_url === null) {
            $closed_captions_url = '';
        }

        $id = add_video($title, $cat, '', $url, $thumb_url, 1, $allow_rating, $allow_comments_reviews, $allow_trackbacks, $notes, $length, $width, $height, $closed_captions_url, null, $time);
        $path = convert_url_to_path($url);
        if ($path !== null) {
            $exif = get_exif_data($path, $filename);
            store_exif('video', strval($id), $exif);
        }
        if (addon_installed('content_privacy')) {
            require_code('content_privacy2');
            save_privacy_form_fields('video', strval($id), $privacy_level, $additional_access);
        }

        require_code('users2');
        if ((has_actual_page_access(get_modal_user(), 'galleries')) && (has_category_access(get_modal_user(), 'galleries', $cat))) {
            $privacy_ok = true;
            if (addon_installed('content_privacy')) {
                require_code('content_privacy');
                $privacy_ok = has_privacy_access('video', strval($id), $GLOBALS['FORUM_DRIVER']->get_guest_id());
            }
            if ($privacy_ok) {
                require_code('syndication');
                syndicate_content('video', strval($id), [['galleries:ACTIVITY_ADD_VIDEO', get_member(), ($title == '') ? basename($url) : $title]]);
            }
        }

        $ret = ['video', $id];
    } else {
        // Image...

        // Images cleanup pipeline
        $maximum_dimension = intval(get_option('maximum_image_size'));
        $watermark = (post_param_integer('watermark', 0) == 1);
        $watermarks = $watermark ? find_gallery_watermarks($_cat) : null;
        if (url_is_local($url)) {
            handle_images_cleanup_pipeline(get_custom_file_base() . '/' . rawurldecode($url), null, IMG_RECOMPRESS_LOSSLESS, $maximum_dimension, $watermarks);
        }

        list(
            $title,
            $cat,
        ) = image_get_default_metadata($url, $filename, '', $_cat);

        if (($title == '') && (get_option('gallery_media_title_required') == '2')) {
            if ($filename !== null) {
                $title = pathinfo($filename, PATHINFO_FILENAME);
            } else {
                $title = $fallback_title;
            }
        }

        $id = add_image($title, $cat, '', $url, 1, $allow_rating, $allow_comments_reviews, $allow_trackbacks, $notes, null, $time);
        $path = convert_url_to_path($url);
        if ($path !== null) {
            $exif = get_exif_data($path, $filename);
            store_exif('image', strval($id), $exif);
        }
        if (addon_installed('content_privacy')) {
            require_code('content_privacy2');
            save_privacy_form_fields('image', strval($id), $privacy_level, $additional_access);
        }

        require_code('users2');
        if ((has_actual_page_access(get_modal_user(), 'galleries')) && (has_category_access(get_modal_user(), 'galleries', $cat))) {
            $privacy_ok = true;
            if (addon_installed('content_privacy')) {
                require_code('content_privacy');
                $privacy_ok = has_privacy_access('image', strval($id), $GLOBALS['FORUM_DRIVER']->get_guest_id());
            }
            if ($privacy_ok) {
                require_code('syndication');
                syndicate_content('image', strval($id), [['galleries:ACTIVITY_ADD_IMAGE', get_member(), ($title == '') ? basename($url) : $title]]);
            }
        }

        $ret = ['image', $id];
    }

    return $ret;
}
