<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing 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  ocProducts Ltd
 * @package    downloads
 */

/*EXTRA FUNCTIONS: shell_exec*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__downloads2()
{
    global $PT_PAIR_CACHE;
    $PT_PAIR_CACHE = array();
}

/**
 * Farm out the files for downloads.
 */
function download_gateway_script()
{
    require_code('downloads');

    $id = get_param_integer('id');
    $result = $GLOBALS['SITE_DB']->query_select('download_downloads', array('name', 'url_redirect'), array('id' => $id), '', 1);
    if (!isset($result[0])) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'download'));
    }

    $name = $result[0]['name'];

    $url = $result[0]['url_redirect'];

    $download_url = generate_dload_url($id, false);

    if (!looks_like_url($url)) {
        list($zone, $attributes) = page_link_decode($url);
        $url = find_script('iframe') . '?zone=' . urlencode($zone);
        if (count($attributes) > 0) {
            $url .= '&' . http_build_query($attributes);
        }
        $keep = symbol_tempcode('KEEP', array('0', '1'));
        $url .= $keep->evaluate();
    }

    attach_to_screen_header('<meta http-equiv="refresh" content="2; URL=' . $download_url->evaluate() . '">');

    attach_to_screen_header('<meta name="robots" content="noindex" />'); // XHTMLXHTML

    if ($url != '') {
        require_lang('downloads');
        $title = get_screen_title('DOWNLOAD_GATEWAY', true, array(escape_html($name)));
        $tpl = do_template('DOWNLOAD_GATEWAY_SCREEN', array('_GUID' => 'ed996e64c34d2c26e43712ffd62c5236', 'TITLE' => $title, 'NAME' => $name, 'ID' => strval($id), 'DOWNLOAD_URL' => $download_url, 'URL' => $url));
        $tpl_wrapped = globalise($tpl, null, '', true, true);
        $tpl_wrapped->evaluate_echo();
    } else {
        header('Location:' . escape_header($download_url->evaluate()));
    }
}

/**
 * Farm out the files for downloads.
 */
function dload_script()
{
    // Closed site
    $site_closed = get_option('site_closed');
    if (($site_closed == '1') && (!has_privilege(get_member(), 'access_closed_site')) && (!$GLOBALS['IS_ACTUALLY_ADMIN'])) {
        header('Content-type: text/plain; charset=' . get_charset());
        @exit(get_option('closed'));
    }

    global $SITE_INFO;
    if ((!is_guest()) || (!isset($SITE_INFO['any_guest_cached_too'])) || ($SITE_INFO['any_guest_cached_too'] == '0')) {
        if ((get_param_string('for_session', '') != md5(get_session_id())) && (get_option('anti_leech') == '1') && (cms_srv('HTTP_REFERER') != '')) {
            warn_exit(do_lang_tempcode('LEECH_BLOCK'));
        }
    }

    require_lang('downloads');

    $id = get_param_integer('id', 0);

    // Lookup
    $rows = $GLOBALS['SITE_DB']->query_select('download_downloads', array('*'), array('id' => $id), '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'download'));
    }
    $myrow = $rows[0];

    // Permission
    if (!has_category_access(get_member(), 'downloads', strval($myrow['category_id']))) {
        access_denied('CATEGORY_ACCESS');
    }
    $may_download = has_privilege(get_member(), 'download', 'downloads', array('downloads', strval($myrow['category_id'])));
    if (!$may_download) {
        access_denied('PRIVILEGE', 'download');
    }
    if (addon_installed('content_privacy')) {
        require_code('content_privacy');
        check_privacy('download', strval($id));
    }

    // Cost?
    $got_before = $GLOBALS['SITE_DB']->query_select_value_if_there('download_logging', 'member_id', array('member_id' => get_member(), 'id' => $id));
    if (addon_installed('points')) {
        if ($myrow['download_cost'] > 0) {
            require_code('points2');

            $member = get_member();
            if (is_guest($member)) {
                access_denied('NOT_AS_GUEST');
            }

            // Check they haven't downloaded this before (they only get charged once - maybe they are resuming)
            if (is_null($got_before)) {
                $cost = $myrow['download_cost'];

                $member = get_member();
                if (is_guest($member)) {
                    access_denied('NOT_AS_GUEST');
                }

                $dif = $cost - available_points($member);
                if (($dif > 0) && (!has_privilege(get_member(), 'have_negative_gift_points'))) {
                    require_lang('points');
                    warn_exit(do_lang_tempcode('LACKING_POINTS', escape_html(integer_format($dif))));
                }
                require_code('points2');
                charge_member($member, $cost, do_lang('DOWNLOADED_THIS', get_translated_text($myrow['name'])));

                if ($myrow['download_submitter_gets_points'] == 1) {
                    system_gift_transfer(do_lang('THEY_DOWNLOADED_THIS', get_translated_text($myrow['name'])), $cost, $myrow['submitter']);
                }
            }
        }
    }

    // Filename
    $full = $myrow['url'];
    require_code('files');
    $extension = strtolower(get_file_extension($full));
    if (url_is_local($full)) {
        $_full = get_custom_file_base() . '/' . rawurldecode(/*filter_naughty*/($full));
    } else {
        $_full = rawurldecode($full);
    }

    // Send header
    if ((strpos($myrow['original_filename'], "\n") !== false) || (strpos($myrow['original_filename'], "\r") !== false)) {
        log_hack_attack_and_exit('HEADER_SPLIT_HACK');
    }
    require_code('mime_types');
    $mime_type = get_mime_type(get_file_extension($myrow['original_filename']), false);
    if (get_option('immediate_downloads') == '1' && $mime_type != 'application/octet-stream') {
        header('Content-Type: ' . $mime_type . '; authoritative=true');
        header('Content-Disposition: inline; filename="' . escape_header($myrow['original_filename'], true) . '"');
    } else {
        header('Content-Type: application/octet-stream' . '; authoritative=true');
        header('Content-Disposition: attachment; filename="' . escape_header($myrow['original_filename'], true) . '"');
    }

    // Is it non-local? If so, redirect
    if ((!url_is_local($full)) || (!file_exists(get_file_base() . '/' . rawurldecode(filter_naughty($full))))) {
        if (url_is_local($full)) {
            $full = get_custom_base_url() . '/' . $full;
        }
        if ((strpos($full, "\n") !== false) || (strpos($full, "\r") !== false)) {
            log_hack_attack_and_exit('HEADER_SPLIT_HACK');
        }
        header('Location: ' . escape_header($full));
        log_download($id, 0, !is_null($got_before)); // Bandwidth used is 0 for an external download
        return;
    }

    // Some basic security: don't fopen php files
    if ($extension == 'php') {
        log_hack_attack_and_exit('PHP_DOWNLOAD_INNOCENT', integer_format($id));
    }

    // Size, bandwidth
    if (!is_file($_full)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
    }
    $size = filesize($_full);
    if (is_null($got_before)) {
        $bandwidth = $GLOBALS['SITE_DB']->query_value_if_there('SELECT SUM(file_size) AS answer FROM ' . get_table_prefix() . 'download_logging l LEFT JOIN ' . get_table_prefix() . 'download_downloads d ON l.id=d.id WHERE date_and_time>' . strval(time() - 24 * 60 * 60 * 32) . ' AND date_and_time<=' . strval(time()));
        if ((($bandwidth + floatval($size)) > (floatval(get_option('maximum_download')) * 1024 * 1024 * 1024)) && (!has_privilege(get_member(), 'bypass_bandwidth_restriction'))) {
            warn_exit(do_lang_tempcode('TOO_MUCH_DOWNLOAD'));
        }

        require_code('files2');
        check_shared_bandwidth_usage($size);
    }

    header('Accept-Ranges: bytes');

    // Caching
    header('Pragma: private');
    header('Cache-Control: private');
    header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 60 * 60 * 24 * 365) . ' GMT');
    $time = is_null($myrow['edit_date']) ? $myrow['add_date'] : $myrow['edit_date'];
    $time = max($time, filemtime($_full));
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $time) . ' GMT');

    // Default to no resume
    $from = 0;
    $new_length = $size;

    safe_ini_set('zlib.output_compression', 'Off'); // So ranges work, plus workaround to bugs caused by IE being 'smart' http://blogs.msdn.com/b/ieinternals/archive/2014/10/21/http-compression-optimize-file-formats-with-deflate.aspx

    // They're trying to resume (so update our range)
    $httprange = cms_srv('HTTP_RANGE');
    if (strlen($httprange) > 0) {
        $_range = explode('=', cms_srv('HTTP_RANGE'));
        if (count($_range) == 2) {
            if (strpos($_range[0], '-') === false) {
                $_range = array_reverse($_range);
            }
            $range = $_range[0];
            if (substr($range, 0, 1) == '-') {
                $range = strval($size - intval(substr($range, 1)) - 1) . $range;
            }
            if (substr($range, -1, 1) == '-') {
                $range .= strval($size - 1);
            }
            $bits = explode('-', $range);
            if (count($bits) == 2) {
                list($from, $to) = array_map('intval', $bits);
                if (($to - $from != 0) || ($from == 0)) { // Workaround to weird behaviour on Chrome
                    $new_length = $to - $from + 1;

                    header('HTTP/1.1 206 Partial Content');
                    header('Content-Range: bytes ' . $range . '/' . strval($size));
                } else {
                    $from = 0;
                }
            }
        }
    }
    header('Content-Length: ' . strval($new_length));
    if (php_function_allowed('set_time_limit')) {
        @set_time_limit(0);
    }
    error_reporting(0);

    if (cms_srv('REQUEST_METHOD') == 'HEAD') {
        return;
    }

    if ($from == 0) {
        log_download($id, $size, !is_null($got_before));
    }

    // Send actual data
    $myfile = fopen($_full, 'rb');
    fseek($myfile, $from);
    if ($size == $new_length) {
        cms_ob_end_clean();
        fpassthru($myfile);
    } else {
        $i = 0;
        flush(); // LEGACY Works around weird PHP bug that sends data before headers, on some PHP versions
        while ($i < $new_length) {
            $content = fread($myfile, min($new_length - $i, 1048576));
            echo $content;
            $len = strlen($content);
            if ($len == 0) {
                break;
            }
            $i += $len;
        }
        fclose($myfile);
    }
    /*

    Security note... at the download adding/editing stage, we ensured that
    only files accessible to the web server (in raw form) could end up in
    our database.
    Therefore we did not check here that our file was accessible in raw
    form.

    */
}

/**
 * Add a download category
 *
 * @param  SHORT_TEXT $category The name of the download category
 * @param  AUTO_LINK $parent_id The parent download category
 * @param  LONG_TEXT $description A description
 * @param  LONG_TEXT $notes Hidden notes pertaining to this download category
 * @param  URLPATH $rep_image The representative image for the category (blank: none)
 * @param  ?AUTO_LINK $id Force an ID (null: don't force an ID)
 * @param  ?TIME $add_time Add time (null: now)
 * @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)
 * @return AUTO_LINK The ID of the newly added download category
 */
function add_download_category($category, $parent_id, $description, $notes = '', $rep_image = '', $id = null, $add_time = null, $meta_keywords = '', $meta_description = '')
{
    require_code('global4');
    prevent_double_submit('ADD_DOWNLOAD_CATEGORY', null, $category);

    if (is_null($add_time)) {
        $add_time = time();
    }

    $map = array(
        'rep_image' => $rep_image,
        'add_date' => $add_time,
        'notes' => $notes,
        'parent_id' => $parent_id,
    );
    $map += insert_lang('category', $category, 2);
    $map += insert_lang_comcode('description', $description, 2);
    if (!is_null($id)) {
        $map['id'] = $id;
    }
    $id = $GLOBALS['SITE_DB']->query_insert('download_categories', $map, true);

    reorganise_uploads__download_categories(array('id' => $id));

    log_it('ADD_DOWNLOAD_CATEGORY', strval($id), $category);

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

    require_code('seo2');
    if (($meta_keywords == '') && ($meta_description == '')) {
        seo_meta_set_for_implicit('downloads_category', strval($id), array($category, $description), $description);
    } else {
        seo_meta_set_for_explicit('downloads_category', strval($id), $meta_keywords, $meta_description);
    }

    if (!is_null($parent_id)) {
        require_code('notifications2');
        copy_notifications_to_new_child('download', strval($parent_id), strval($id));
    }

    require_code('member_mentions');
    dispatch_member_mention_notifications('download_category', strval($id));

    require_code('sitemap_xml');
    notify_sitemap_node_add('_SEARCH:downloads:browse:' . strval($id), $add_time, null, SITEMAP_IMPORTANCE_MEDIUM, 'weekly', has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'downloads', strval($id)));

    return $id;
}

/**
 * Edit the given download category with the new details given
 *
 * @param  AUTO_LINK $category_id The ID of the category being edited
 * @param  SHORT_TEXT $category The name of the download category
 * @param  AUTO_LINK $parent_id The parent download category
 * @param  LONG_TEXT $description A description
 * @param  LONG_TEXT $notes Hidden notes pertaining to this download category
 * @param  URLPATH $rep_image The representative image for the category (blank: none)
 * @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  ?TIME $add_time Add time (null: do not change)
 */
function edit_download_category($category_id, $category, $parent_id, $description, $notes, $rep_image, $meta_keywords, $meta_description, $add_time = null)
{
    $under_category_id = $parent_id;
    while ((!is_null($under_category_id)) && ($under_category_id != INTEGER_MAGIC_NULL)) {
        if ($category_id == $under_category_id) {
            warn_exit(do_lang_tempcode('OWN_PARENT_ERROR', 'download_category'));
        }
        $_under_category_id = $GLOBALS['SITE_DB']->query_select_value('download_categories', 'parent_id', array('id' => $under_category_id));
        if ($under_category_id === $_under_category_id) {
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
        }
        $under_category_id = $_under_category_id;
    }

    require_code('urls2');
    suggest_new_idmoniker_for('downloads', 'browse', strval($category_id), '', $category);

    $rows = $GLOBALS['SITE_DB']->query_select('download_categories', array('category', 'description'), array('id' => $category_id), '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'download_category'));
    }
    $_category = $rows[0]['category'];
    $_description = $rows[0]['description'];

    $update_map = array(
        'notes' => $notes,
        'parent_id' => $parent_id,
    );
    $update_map += lang_remap('category', $_category, $category);
    $update_map += lang_remap_comcode('description', $_description, $description);
    if (!is_null($rep_image)) {
        $update_map['rep_image'] = $rep_image;
        require_code('files2');
        delete_upload('uploads/repimages', 'download_categories', 'rep_image', 'id', $category_id, $rep_image);
    }
    if (!is_null($add_time)) {
        $update_map['add_date'] = $add_time;
    }
    $GLOBALS['SITE_DB']->query_update('download_categories', $update_map, array('id' => $category_id), '', 1);

    require_code('seo2');
    seo_meta_set_for_explicit('downloads_category', strval($category_id), $meta_keywords, $meta_description);

    reorganise_uploads__download_categories(array('id' => $category_id));

    log_it('EDIT_DOWNLOAD_CATEGORY', strval($category_id), $category);

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

    require_code('sitemap_xml');
    notify_sitemap_node_edit('_SEARCH:downloads:browse:' . strval($category_id), has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'downloads', strval($category_id)));
}

/**
 * Delete a download category.
 *
 * @param  AUTO_LINK $category_id The download category to delete
 */
function delete_download_category($category_id)
{
    $root_category = $GLOBALS['SITE_DB']->query_select_value('download_categories', 'MIN(id)');
    if ($category_id == $root_category) {
        warn_exit(do_lang_tempcode('NO_DELETE_ROOT', 'download_category'));
    }

    $rows = $GLOBALS['SITE_DB']->query_select('download_categories', array('category', 'description', 'parent_id'), array('id' => $category_id), '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'download_category'));
    }
    $category = $rows[0]['category'];
    $description = $rows[0]['description'];

    require_code('files2');
    delete_upload('uploads/repimages', 'download_categories', 'rep_image', 'id', $category_id);

    if (addon_installed('catalogues')) {
        update_catalogue_content_ref('download_category', strval($category_id), '');
    }

    $GLOBALS['SITE_DB']->query_delete('download_categories', array('id' => $category_id), '', 1);
    $GLOBALS['SITE_DB']->query_update('download_downloads', array('category_id' => $rows[0]['parent_id']), array('category_id' => $category_id));
    $GLOBALS['SITE_DB']->query_update('download_categories', array('parent_id' => $rows[0]['parent_id']), array('parent_id' => $category_id));

    delete_lang($category);
    delete_lang($description);

    require_code('seo2');
    seo_meta_erase_storage('downloads_category', strval($category_id));

    $GLOBALS['SITE_DB']->query_delete('group_category_access', array('module_the_name' => 'downloads', 'category_name' => strval($category_id)));
    $GLOBALS['SITE_DB']->query_delete('group_privileges', array('module_the_name' => 'downloads', 'category_name' => strval($category_id)));

    $GLOBALS['SITE_DB']->query_update('url_id_monikers', array('m_deprecated' => 1), array('m_resource_page' => 'downloads', 'm_resource_type' => 'browse', 'm_resource_id' => strval($category_id)));

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

    log_it('DELETE_DOWNLOAD_CATEGORY', strval($category_id), get_translated_text($category));

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

    require_code('sitemap_xml');
    notify_sitemap_node_delete('_SEARCH:downloads:browse:' . strval($category_id));
}

/**
 * Create a data-mash from the file at a URL. This is data useful for the search engine.
 *
 * @param  URLPATH $url The URL to make a data-mash of, or a filename if $data isn't blank
 * @param  ?string $data Data (null: use URL)
 * @param  ?ID_TEXT $extension File extension (null: get from URL)
 * @param  boolean $direct_path Whether a direct file path was given instead of a URL
 * @return LONG_TEXT The data-mash
 */
function create_data_mash($url, $data = null, $extension = null, $direct_path = false)
{
    if (get_option('dload_search_index') == '0') {
        return '';
    }

    if (running_script('stress_test_loader')) {
        return '';
    }

    if (ini_get('memory_usage') == '8M') {
        return ''; // Some cowardice... don't want to tempt fate
    }

    if (is_null($extension)) {
        $extension = get_file_extension($url);
    }

    $tmp_file = null;

    if (is_null($data)) {
        if (($direct_path) || (url_is_local($url))) {
            $actual_path = $direct_path ? $url : get_custom_file_base() . '/' . rawurldecode($url);

            if (file_exists($actual_path)) {
                switch ($extension) {
                    case 'zip':
                    case 'odt':
                    case 'odp':
                    case 'docx':
                    case 'tar':
                    case 'gz':
                        if (filesize($actual_path) > 1024 * 1024 * 3) {
                            return '';
                        }
                        break;
                }

                $tmp_file = $actual_path;
                if (filesize($actual_path) > 1024 * 1024 * 3) {
                    $myfile = fopen($actual_path, 'rb');
                    flock($myfile, LOCK_SH);
                    $data = '';
                    for ($i = 0; $i < 384; $i++) {
                        $data .= fread($myfile, 8192);
                    }
                    flock($myfile, LOCK_UN);
                    fclose($myfile);
                } else {
                    $data = file_get_contents($actual_path);
                }
            } else {
                $data = '';
            }
        } else {
            switch ($extension) {
                case 'txt':
                case '1st':
                case 'rtf':
                case 'pdf':
                case 'htm':
                case 'html':
                case 'xml':
                case 'doc':
                case 'xls':
                    break; // Continue through to download good stuff

                default:
                    return ''; // Don't download, it's not worth it
                    break;
            }

            $data = http_download_file($url, 3 * 1024 * 1024, false); // 3MB is enough
            if (is_null($data)) {
                return '';
            }
        }
    }

    $mash = '';

    switch ($extension) {
        case 'zip':
        case 'odt':
        case 'odp':
        case 'docx':
            require_code('m_zip');
            $tmp_file = cms_tempnam();
            $myfile2 = fopen($tmp_file, 'wb');
            fwrite($myfile2, $data);
            fclose($myfile2);
            $myfile_zip = @zip_open($tmp_file);
            if (!is_integer($myfile_zip)) {
                while (($entry = (@zip_read($myfile_zip))) !== false) { // Temporary file may be cleaned up before this can complete, hence @
                    $entry_name = @zip_entry_name($entry);
                    $mash .= ' ' . $entry_name;
                    if (substr($entry_name, -1) != '/') {
                        $_entry = @zip_entry_open($myfile_zip, $entry);
                        if ($_entry !== false) {
                            $file_data = '';
                            while (true) {
                                $it = @zip_entry_read($entry, 1024);
                                if (($it === false) || ($it == '')) {
                                    break;
                                }
                                $file_data .= $it;
                                if (strlen($file_data) >= 3 * 1024 * 1024) {
                                    break; // 3MB is enough
                                }
                            }
                            @zip_entry_close($entry);
                            $mash .= ' ' . create_data_mash($entry_name, $file_data);
                            if (strlen($mash) >= 3 * 1024 * 1024) {
                                break; // 3MB is enough
                            }
                        }
                    }
                }
                @zip_close($myfile_zip);
            }
            @unlink($tmp_file);
            break;
        case 'tar':
            require_code('tar');
            $tmp_file = cms_tempnam();
            $myfile = fopen($tmp_file, 'wb');
            fwrite($myfile, $data);
            fclose($myfile);
            $myfile_tar = tar_open($tmp_file, 'rb');
            if ($myfile_tar !== false) {
                $directory = tar_get_directory($myfile_tar);
                foreach ($directory as $entry) {
                    $entry_name = $entry['path'];
                    $mash .= ' ' . $entry_name;
                    if ($entry['size'] >= 3 * 1024 * 1024) {
                        continue; // 3MB is enough
                    }
                    $_entrya = tar_get_file($myfile_tar, $entry['path']);
                    if (!is_null($_entrya)) {
                        $mash .= ' ' . create_data_mash($entry_name, $_entrya['data']);
                        if (strlen($mash) >= 3 * 1024 * 1024) {
                            break; // 3MB is enough
                        }
                    }
                }
                tar_close($myfile_tar);
            }
            @unlink($tmp_file);
            break;
        case 'gz':
            if (function_exists('gzopen')) {
                if (function_exists('gzeof')) {
                    if (function_exists('gzread')) {
                        $tmp_file = cms_tempnam();
                        $myfile = fopen($tmp_file, 'wb');
                        fwrite($myfile, $data);
                        fclose($myfile);
                        $myfile = gzopen($tmp_file, 'rb');
                        if ($myfile !== false) {
                            $file_data = '';
                            while (!gzeof($myfile)) {
                                $it = gzread($myfile, 1024);
                                $file_data .= $it;
                                if (strlen($file_data) >= 3 * 1024 * 1024) {
                                    break; // 3MB is enough
                                }
                            }
                            $mash = ' ' . create_data_mash(preg_replace('#\.gz#i', '', $url), $file_data);
                        }
                        @unlink($tmp_file);
                    }
                }
            }
            break;
        case 'txt':
        case '1st':
            $mash .= $data;
            break;
        case 'rtf':
            $len = strlen($data);
            $skipping_section_depth = 0;
            $escape = false;
            for ($i = 0; $i < $len; $i++) {
                $byte = $data[$i];
                if ((!$escape) && ($byte == "\\")) {
                    $escape = true;
                } elseif ((!$escape) && ($byte == '{')) {
                    if ($skipping_section_depth != 0) {
                        $skipping_section_depth++;
                    }
                } elseif ((!$escape) && ($byte == '}')) {
                    if ($skipping_section_depth != 0) {
                        $skipping_section_depth--;
                    }
                } elseif (($escape) && ($byte != '{') && ($byte != "\\") && ($byte != '}')) {
                    $end_pos_1 = strpos($data, "\\", $i + 1);
                    if ($end_pos_1 === false) {
                        $end_pos_1 = $len;
                    }
                    $end_pos_2 = strpos($data, "\n", $i + 1);
                    if ($end_pos_2 === false) {
                        $end_pos_2 = $len;
                    }
                    $end_pos_3 = strpos($data, ' ', $i + 1);
                    if ($end_pos_3 === false) {
                        $end_pos_3 = $len;
                    }
                    $end_pos_4 = strpos($data, "\t", $i + 1);
                    if ($end_pos_4 === false) {
                        $end_pos_4 = $len;
                    }
                    $end_pos_5 = strpos($data, '{', $i + 1);
                    if ($end_pos_5 === false) {
                        $end_pos_5 = $len;
                    }
                    $end_pos_6 = strpos($data, '}', $i + 1);
                    if ($end_pos_6 === false) {
                        $end_pos_6 = $len;
                    }
                    $end_pos = min($end_pos_1, $end_pos_2, $end_pos_3, $end_pos_4, $end_pos_5, $end_pos_6);
                    $tag = substr($data, $i, $end_pos - $i);
                    $tag = preg_replace('#[\-0-9]*#', '', $tag);
                    if (($skipping_section_depth == 0) && (($tag == 'pgdsc') || ($tag == 'comment') || ($tag == 'object') || ($tag == 'pict') || ($tag == 'stylesheet') || ($tag == 'fonttbl'))) {
                        $skipping_section_depth = 1;
                    }
                    if ($tag == 'par') {
                        $mash .= "\n";
                    }
                    $i = $end_pos - 1;
                    $escape = false;
                } elseif ($skipping_section_depth == 0) {
                    if (($byte != "\r") && ($byte != "\n")) {
                        $mash .= $byte;
                    }
                    $escape = false;
                } else {
                    $escape = false;
                }
            }
            break;
        case 'pdf':
            if ((str_replace(array('on', 'true', 'yes'), array('1', '1', '1'), strtolower(ini_get('safe_mode'))) != '1') && (php_function_allowed('shell_exec')) && (!is_null($tmp_file))) {
                $enc = (get_charset() == 'utf-8') ? ' -enc UTF-8' : '';
                $path = 'pdftohtml -i -noframes -stdout -hidden' . $enc . ' -q -xml ' . escapeshellarg_wrap($tmp_file);
                if (stripos(PHP_OS, 'WIN') === 0) {
                    if (file_exists(get_file_base() . '/data_custom/pdftohtml.exe')) {
                        $path = '"' . get_file_base() . '/data_custom/' . '"' . $path;
                    }
                }
                $tmp_file_2 = cms_tempnam();
                @shell_exec($path . ' > ' . $tmp_file_2);
                $mash = create_data_mash($tmp_file_2, null, 'xml', true);
                @unlink($tmp_file_2);
            }
            break;
        case 'htm':
        case 'html':
            $head_patterns = array('#<\s*script.*<\s*/\s*script\s*>#misU', '#<\s*link[^<>]*>#misU', '#<\s*style.*<\s*/\s*style\s*>#misU');
            foreach ($head_patterns as $pattern) {
                $data = preg_replace($pattern, '', $data);
            }
        // intentionally rolls on...
        case 'xml':
            $mash = str_replace('&apos;', '\'', str_replace(' false ', ' ', str_replace(' true ', ' ', @html_entity_decode(preg_replace('#\<[^\<\>]*\>#', ' ', $data), ENT_QUOTES, get_charset()))));
            $mash = preg_replace('#Error : Bad \w+#', '', $mash);
            break;
        case 'xls':
        case 'doc':
        case 'ppt':
        case 'hlp':
            //default: // Binary formats are complex to parse, but whatsmore, as textual tagging isn't used, extraction can be done automatically as all identified text is good.
            $data = str_replace("\0", '', $data); // Strip out interleaved nulls because they are used in wide-chars, obscuring the data
            $mash = '';
            $needs_delimiter_next = false;
            $in_portion = false;
            $min_length = 10;
            if ($extension == 'xls') {
                $min_length = 4;
            }
            for ($i = 0; $i < strlen($data); $i++) {
                $ch = $data[$i];
                $chx = 1;
                $next_ok = _is_valid_data_mash_char($ch);
                if (($next_ok) && (!$in_portion)) {
                    $x = $ch;
                    for ($j = $i + 1; $j < strlen($data); $j++) { // Count how far a new word goes
                        $_ch = $data[$j];
                        $_next_ok = _is_valid_data_mash_char($_ch);
                        if ($_next_ok) {
                            $x .= $_ch;
                            $chx++;
                        } else {
                            break;
                        }
                    }
                    if ((strlen($x) < $min_length) || ($x == strtoupper($x)) || ($x == 'Microsoft Word Document') || ($x == 'WordDocument') || ($x == 'SummaryInformation') || ($x == 'DocumentSummaryInformation')) { // Valid word okay
                        $i = $j;
                        continue;
                    }
                }

                if (($next_ok) && ($in_portion)) {
                    $mash .= $ch;
                } elseif (($next_ok) && ($chx >= $min_length)) {
                    if ($needs_delimiter_next) {
                        $mash .= ' ';
                        $needs_delimiter_next = false;
                    }
                    $mash .= $ch;
                    $in_portion = true;
                } else {
                    if ($in_portion) {
                        $needs_delimiter_next = true;
                        $in_portion = false;
                    }
                }
            }
            break;
    }

    if (strlen($mash) > 1024 * 1024 * 3) {
        $mash = substr($mash, 0, 1024 * 1024 * 3);
    }
    $mash = preg_replace('# +#', ' ', preg_replace('#[^\w\-\']#', ' ', $mash));
    if (strlen($mash) > intval(1024 * 1024 * 1 * 0.4)) {
        $mash = substr($mash, 0, intval(1024 * 1024 * 0.4));
    }

    return $mash;
}

/**
 * Find if a character is basically a part of a text string.
 *
 * @param  string $ch Character to test
 * @return boolean Whether the character is valid
 *
 * @ignore
 */
function _is_valid_data_mash_char(&$ch)
{
    $c = ord($ch);
    if (($c == 145) || ($c == 146)) {
        $ch = "'";
    }
    return (($c >= 65 && $c <= 90) || ($c >= 97 && $c <= 122) || ($ch == "'") || ($ch == '-'));
}

/**
 * Add a download.
 *
 * @param  AUTO_LINK $category_id The ID of the category the download is to be in
 * @param  SHORT_TEXT $name The name of the download
 * @param  URLPATH $url The URL to the download
 * @param  LONG_TEXT $description The description of the download
 * @param  ID_TEXT $author The author of the download (not necessarily same as the submitter)
 * @param  LONG_TEXT $additional_details The supplementary description for the download
 * @param  ?AUTO_LINK $out_mode_id The out-mode-id (the ID of a download that this download is an old version of). Often people wonder why this is specified with the old version, and not the opposite with the new version - it is because statistically, we perceive more chance of downloads merging than splitting (null: none)
 * @param  BINARY $validated Whether the download has been validated
 * @param  BINARY $allow_rating Whether the download may be rated
 * @param  SHORT_INTEGER $allow_comments Whether comments are allowed (0=no, 1=yes, 2=review style)
 * @param  BINARY $allow_trackbacks Whether the download may be trackbacked
 * @param  LONG_TEXT $notes Hidden notes pertaining to the download
 * @param  SHORT_TEXT $original_filename The downloads original filename (the URL may be obfuscated)
 * @param  integer $file_size The file size of the download (we can't really detect this in real-time for remote URLs)
 * @param  integer $cost The cost of the download that members will have to pay to get it
 * @param  BINARY $submitter_gets_points Whether the submitter gets the points for the download (they are selling it) (otherwise they are just thrown out, which is an alternative model - one of enforcing community point building)
 * @param  ?AUTO_LINK $licence The licence to use (null: none)
 * @param  ?TIME $add_date The add date for the download (null: now)
 * @param  integer $num_downloads The number of downloads that this download has had
 * @param  integer $views The number of views that this download has had
 * @param  ?MEMBER $submitter The submitter (null: current user)
 * @param  ?TIME $edit_date The edit date (null: never)
 * @param  ?AUTO_LINK $id Force an ID (null: don't force an ID)
 * @param  SHORT_TEXT $meta_keywords Meta keywords for this resource (blank: implicit)
 * @param  LONG_TEXT $meta_description Meta description for this resource (blank: implicit)
 * @param  integer $default_pic The ordered number of the gallery image to use as the download representative image
 * @param  URLPATH $url_redirect The URL to redirect
 * @return AUTO_LINK The ID of the newly added download
 */
function add_download($category_id, $name, $url, $description, $author, $additional_details, $out_mode_id, $validated, $allow_rating, $allow_comments, $allow_trackbacks, $notes, $original_filename, $file_size, $cost, $submitter_gets_points, $licence = null, $add_date = null, $num_downloads = 0, $views = 0, $submitter = null, $edit_date = null, $id = null, $meta_keywords = '', $meta_description = '', $default_pic = 1, $url_redirect = '')
{
    require_code('global4');
    prevent_double_submit('ADD_DOWNLOAD', null, $name);

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

    if (is_null($submitter)) {
        $submitter = get_member();
    }

    if (($file_size == 0) || (url_is_local($url))) {
        if (url_is_local($url)) {
            $file_size = @filesize(get_custom_file_base() . '/' . rawurldecode($url)) or $file_size = null;
        } else {
            http_download_file($url, 0, false);
            $file_size = $GLOBALS['HTTP_DOWNLOAD_SIZE'];
        }
    }

    if (($file_size < 0) || ($file_size > 2147483647)) { // TODO: #3046 in tracker
        $file_size = 2147483647;
    }

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

    $map = array(
        'download_data_mash' => '',
        'download_licence' => $licence,
        'rep_image' => '',
        'edit_date' => $edit_date,
        'download_submitter_gets_points' => $submitter_gets_points,
        'download_cost' => $cost,
        'original_filename' => $original_filename,
        'download_views' => $views,
        'allow_rating' => $allow_rating,
        'allow_comments' => $allow_comments,
        'allow_trackbacks' => $allow_trackbacks,
        'notes' => $notes,
        'submitter' => $submitter,
        'default_pic' => $default_pic,
        'num_downloads' => $num_downloads,
        'out_mode_id' => $out_mode_id,
        'category_id' => $category_id,
        'url' => $url,
        'author' => cms_mb_substr($author, 0, 80),
        'url_redirect' => $url_redirect,
        'validated' => $validated,
        'add_date' => $add_date,
        'file_size' => $file_size,
    );
    $map += insert_lang('name', $name, 2);
    $map += insert_lang_comcode('description', $description, 3);
    $map += insert_lang_comcode('additional_details', $additional_details, 3);

    if (!is_null($id)) {
        $map['id'] = $id;
        $GLOBALS['SITE_DB']->query_insert('download_downloads', $map);
    } else {
        $id = $GLOBALS['SITE_DB']->query_insert('download_downloads', $map, true);
    }

    require_code('tasks');
    require_lang('downloads');
    call_user_func_array__long_task(do_lang('INDEX_DOWNLOAD'), get_screen_title('INDEX_DOWNLOAD', true, null, null, null, false), 'index_download', array($id, $url, $original_filename), false, false, false);

    require_code('seo2');
    if (($meta_keywords == '') && ($meta_description == '')) {
        seo_meta_set_for_implicit('downloads_download', strval($id), array($name, $description, $additional_details), $description);
    } else {
        seo_meta_set_for_explicit('downloads_download', strval($id), $meta_keywords, $meta_description);
    }

    // Make its gallery
    if ((addon_installed('galleries')) && (!running_script('stress_test_loader'))) {
        $test = $GLOBALS['SITE_DB']->query_select_value_if_there('galleries', 'name', array('name' => 'download_' . strval($id)));
        if (is_null($test)) {
            require_lang('downloads');
            require_code('galleries2');
            $download_gallery_root = get_option('download_gallery_root');
            add_gallery('download_' . strval($id), do_lang('GALLERY_FOR_DOWNLOAD', $name), '', '', $download_gallery_root);
            set_download_gallery_permissions($id, $submitter);
        }
    }

    // Stat
    update_stat('num_archive_downloads', 1);
    if ($file_size > 0) {
        update_stat('archive_size', $file_size);
    }

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

        require_lang('downloads');
        require_code('notifications');
        $subject = do_lang('DOWNLOAD_NOTIFICATION_MAIL_SUBJECT', get_site_name(), $name);
        $self_url = build_url(array('page' => 'downloads', 'type' => 'entry', 'id' => $id), get_module_zone('downloads'), null, false, false, true);
        $mail = do_notification_lang('DOWNLOAD_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape($name), array(comcode_escape($self_url->evaluate())));
        dispatch_notification('download', strval($category_id), $subject, $mail, $privacy_limits);
    }

    reorganise_uploads__downloads(array('id' => $id));

    log_it('ADD_DOWNLOAD', strval($id), $name);

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

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

    require_code('sitemap_xml');
    notify_sitemap_node_add('_SEARCH:downloads:entry:' . strval($id), $add_date, $edit_date, SITEMAP_IMPORTANCE_HIGH, 'monthly', has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'downloads', strval($category_id)));

    return $id;
}

/**
 * Set the permissions for a download gallery.
 *
 * @param  ?AUTO_LINK $id The ID of the download (null: lookup from download)
 * @param  ?MEMBER $submitter The submitter (null: work out automatically)
 */
function set_download_gallery_permissions($id, $submitter = null)
{
    if (is_null($submitter)) {
        $submitter = $GLOBALS['SITE_DB']->query_select_value('download_downloads', 'submitter', array('id' => $id));
    }

    $download_gallery_root = get_option('download_gallery_root');

    // Copy through requisite permissions
    $GLOBALS['SITE_DB']->query_delete('group_category_access', array('module_the_name' => 'galleries', 'category_name' => 'download_' . strval($id)));
    $perms = $GLOBALS['SITE_DB']->query_select('group_category_access', array('*'), array('module_the_name' => 'galleries', 'category_name' => $download_gallery_root));
    foreach ($perms as $perm) {
        $perm['category_name'] = 'download_' . strval($id);
        $GLOBALS['SITE_DB']->query_insert('group_category_access', $perm);
    }
    $GLOBALS['SITE_DB']->query_delete('group_privileges', array('module_the_name' => 'galleries', 'category_name' => 'download_' . strval($id)));
    $perms = $GLOBALS['SITE_DB']->query_select('group_privileges', array('*'), array('module_the_name' => 'galleries', 'category_name' => $download_gallery_root));
    foreach ($perms as $perm) {
        $perm['category_name'] = 'download_' . strval($id);
        $GLOBALS['SITE_DB']->query_insert('group_privileges', $perm);
    }
    // If they were able to submit the download, they should be able to submit extra images
    $GLOBALS['SITE_DB']->query_delete('member_privileges', array('module_the_name' => 'galleries', 'category_name' => 'download_' . strval($id)));
    foreach (array('submit_midrange_content') as $privilege) {
        $GLOBALS['SITE_DB']->query_insert('member_privileges', array('active_until' => null, 'member_id' => $submitter, 'privilege' => $privilege, 'the_page' => '', 'module_the_name' => 'galleries', 'category_name' => 'download_' . strval($id), 'the_value' => 1));
    }
}

/**
 * Edit a download.
 *
 * @param  AUTO_LINK $id The ID of the download to edit
 * @param  AUTO_LINK $category_id The ID of the category the download is to be in
 * @param  SHORT_TEXT $name The name of the download
 * @param  URLPATH $url The URL to the download
 * @param  LONG_TEXT $description The description of the download
 * @param  ID_TEXT $author The author of the download (not necessarily same as the submitter)
 * @param  LONG_TEXT $additional_details The supplementary description for the download
 * @param  ?AUTO_LINK $out_mode_id The out-mode-id (the ID of a download that this download is an old version of). Often people wonder why this is specified with the old version, and not the opposite with the new version - it is because statistically, we perceive more chance of downloads merging than splitting (null: none)
 * @param  integer $default_pic The ordered number of the gallery image to use as the download representative image
 * @param  BINARY $validated Whether the download has been validated
 * @param  BINARY $allow_rating Whether the download may be rated
 * @param  SHORT_INTEGER $allow_comments Whether comments are allowed (0=no, 1=yes, 2=review style)
 * @param  BINARY $allow_trackbacks Whether the download may be trackbacked
 * @param  LONG_TEXT $notes Hidden notes pertaining to the download
 * @param  SHORT_TEXT $original_filename The downloads original filename (the URL may be obfuscated)
 * @param  integer $file_size The file size of the download (we can't really detect this in real-time for remote URLs)
 * @param  integer $cost The cost of the download that members will have to pay to get it
 * @param  BINARY $submitter_gets_points Whether the submitter gets the points for the download (they are selling it) (otherwise they are just thrown out, which is an alternative model - one of enforcing community point building)
 * @param  ?AUTO_LINK $licence The licence to use (null: none)
 * @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  ?integer $num_downloads The number of downloads that this download has had (null: do not change)
 * @param  boolean $null_is_literal Determines whether some nulls passed mean 'use a default' or literally mean 'set to null'
 * @param  URLPATH $url_redirect The URL to redirect
 */
function edit_download($id, $category_id, $name, $url, $description, $author, $additional_details, $out_mode_id, $default_pic, $validated, $allow_rating, $allow_comments, $allow_trackbacks, $notes, $original_filename, $file_size, $cost, $submitter_gets_points, $licence, $meta_keywords, $meta_description, $edit_time = null, $add_time = null, $views = null, $submitter = null, $num_downloads = null, $null_is_literal = false, $url_redirect = '')
{
    if (is_null($edit_time)) {
        $edit_time = $null_is_literal ? null : time();
    }

    require_code('urls2');
    suggest_new_idmoniker_for('downloads', 'view', strval($id), '', $name);

    if (fractional_edit()) {
        $file_size = INTEGER_MAGIC_NULL;
    } else {
        if (($file_size == 0) || (url_is_local($url))) {
            if (url_is_local($url)) {
                $file_size = @filesize(get_custom_file_base() . '/' . rawurldecode($url));
                if ($file_size === false) {
                    $file_size = 0;
                }
            } else {
                http_download_file($url, 0, false);
                $file_size = $GLOBALS['HTTP_DOWNLOAD_SIZE'];
            }
        }
    }

    if (($file_size < 0) || ($file_size > 2147483647)) { // TODO: #3046 in tracker
        $file_size = 2147483647;
    }

    $myrows = $GLOBALS['SITE_DB']->query_select('download_downloads', array('name', 'description', 'additional_details', 'category_id'), array('id' => $id), '', 1);
    if (!array_key_exists(0, $myrows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'download'));
    }
    $myrow = $myrows[0];

    require_code('seo2');
    seo_meta_set_for_explicit('downloads_download', strval($id), $meta_keywords, $meta_description);

    require_code('files2');
    delete_upload('uploads/downloads', 'download_downloads', 'url', 'id', $id, $url);

    if (!fractional_edit()) {
        require_code('tasks');
        require_lang('downloads');
        call_user_func_array__long_task(do_lang('INDEX_DOWNLOAD'), get_screen_title('INDEX_DOWNLOAD', true, null, null, null, false), 'index_download', array($id, $url, $original_filename), false, false, false);
    }

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

    require_code('submit');
    $just_validated = (!content_validated('download', strval($id))) && ($validated == 1);
    if ($just_validated) {
        send_content_validated_notification('download', strval($id));
    }

    $update_map = array(
        'download_licence' => $licence,
        'original_filename' => $original_filename,
        'download_submitter_gets_points' => $submitter_gets_points,
        'download_cost' => $cost,
        'edit_date' => $edit_time,
        'file_size' => $file_size,
        'allow_rating' => $allow_rating,
        'allow_comments' => $allow_comments,
        'allow_trackbacks' => $allow_trackbacks,
        'notes' => $notes,
        'validated' => $validated,
        'category_id' => $category_id,
        'url' => $url,
        'author' => cms_mb_substr($author, 0, 80),
        'url_redirect' => $url_redirect,
        'default_pic' => $default_pic,
        'out_mode_id' => $out_mode_id,
    );
    $update_map += lang_remap('name', $myrow['name'], $name);
    $update_map += lang_remap_comcode('description', $myrow['description'], $description);
    $update_map += lang_remap_comcode('additional_details', $myrow['additional_details'], $additional_details);

    $update_map['edit_date'] = $edit_time;
    if (!is_null($add_time)) {
        $update_map['add_date'] = $add_time;
    }
    if (!is_null($views)) {
        $update_map['download_views'] = $views;
    }
    if (!is_null($submitter)) {
        $update_map['submitter'] = $submitter;
    }
    if (!is_null($num_downloads)) {
        $update_map['num_downloads'] = $num_downloads;
    }

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

    $self_url = build_url(array('page' => 'downloads', 'type' => 'entry', 'id' => $id), get_module_zone('downloads'), null, false, false, true);

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

        require_lang('downloads');
        require_code('notifications');
        $subject = do_lang('DOWNLOAD_NOTIFICATION_MAIL_SUBJECT', get_site_name(), $name);
        $mail = do_notification_lang('DOWNLOAD_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape($name), array(comcode_escape($self_url->evaluate())));
        dispatch_notification('download', strval($category_id), $subject, $mail, $privacy_limits);
    }

    reorganise_uploads__downloads(array('id' => $id));

    log_it('EDIT_DOWNLOAD', strval($id), get_translated_text($myrow['name']));

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

    if (addon_installed('galleries')) {
        // Change its gallery
        require_code('galleries2');
        $download_gallery_root = get_option('download_gallery_root');
        $test = $GLOBALS['SITE_DB']->query_select_value_if_there('galleries', 'parent_id', array('name' => 'download_' . strval($id)));
        if (!is_null($test)) {
            edit_gallery('download_' . strval($id), 'download_' . strval($id), do_lang('GALLERY_FOR_DOWNLOAD', $name), '', '', $download_gallery_root);
        }
    }

    require_code('feedback');
    update_spacer_post(
        $allow_comments != 0,
        'downloads',
        strval($id),
        $self_url,
        $name,
        process_overridden_comment_forum('downloads', strval($id), strval($category_id), strval($myrow['category_id']))
    );

    require_code('sitemap_xml');
    notify_sitemap_node_edit('_SEARCH:downloads:entry:' . strval($id), has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'downloads', strval($category_id)));
}

/**
 * Delete a download.
 *
 * @param  AUTO_LINK $id The ID of the download to delete
 * @param  boolean $leave Whether to leave the actual file behind
 */
function delete_download($id, $leave = false)
{
    $myrows = $GLOBALS['SITE_DB']->query_select('download_downloads', array('name', 'description', 'additional_details'), array('id' => $id), '', 1);
    if (!array_key_exists(0, $myrows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'download'));
    }
    $myrow = $myrows[0];

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

    delete_lang($myrow['name']);
    delete_lang($myrow['description']);
    delete_lang($myrow['additional_details']);

    require_code('seo2');
    seo_meta_erase_storage('downloads_download', strval($id));

    if (!$leave) {
        require_code('files2');
        delete_upload('uploads/downloads', 'download_downloads', 'url', 'id', $id);
    }

    // Delete from database
    $GLOBALS['SITE_DB']->query_delete('download_downloads', array('id' => $id), '', 1);
    $GLOBALS['SITE_DB']->query_delete('download_logging', array('id' => $id));
    $GLOBALS['SITE_DB']->query_delete('rating', array('rating_for_type' => 'downloads', 'rating_for_id' => strval($id)));
    $GLOBALS['SITE_DB']->query_delete('trackbacks', array('trackback_for_type' => 'downloads', 'trackback_for_id' => strval($id)));
    require_code('notifications');
    delete_all_notifications_on('comment_posted', 'downloads_' . strval($id));

    $GLOBALS['SITE_DB']->query_update('download_downloads', array('out_mode_id' => null), array('out_mode_id' => $id), '', 1);

    if (addon_installed('galleries')) {
        // Delete gallery
        $name = 'download_' . strval($id);
        require_code('galleries2');
        $test = $GLOBALS['SITE_DB']->query_select_value_if_there('galleries', 'parent_id', array('name' => 'download_' . strval($id)));
        if (!is_null($test)) {
            delete_gallery($name);
        }
    }

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

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

    log_it('DELETE_DOWNLOAD', strval($id), get_translated_text($myrow['name']));

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

    require_code('sitemap_xml');
    notify_sitemap_node_delete('_SEARCH:downloads:entry:' . strval($id));
}

/**
 * Add a download licence.
 *
 * @param  SHORT_TEXT $title The title of the download licence
 * @param  LONG_TEXT $text The text of the download licence
 * @return AUTO_LINK The ID of the new download licence
 */
function add_download_licence($title, $text)
{
    require_code('global4');
    prevent_double_submit('ADD_DOWNLOAD_LICENCE', null, $title);

    $id = $GLOBALS['SITE_DB']->query_insert('download_licences', array('l_title' => $title, 'l_text' => $text), true);

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

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

    return $id;
}

/**
 * Edit a download licence.
 *
 * @param  AUTO_LINK $id The ID of the download licence to edit
 * @param  SHORT_TEXT $title The title of the download licence
 * @param  LONG_TEXT $text The text of the download licence
 */
function edit_download_licence($id, $title, $text)
{
    $GLOBALS['SITE_DB']->query_update('download_licences', array('l_title' => $title, 'l_text' => $text), array('id' => $id), '', 1);

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

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

/**
 * Delete a download licence.
 *
 * @param  AUTO_LINK $id The ID of the download licence to delete
 */
function delete_download_licence($id)
{
    $myrows = $GLOBALS['SITE_DB']->query_select('download_licences', array('l_title'), array('id' => $id), '', 1);
    if (!array_key_exists(0, $myrows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'download_licence'));
    }
    $myrow = $myrows[0];

    $GLOBALS['SITE_DB']->query_delete('download_licences', array('id' => $id), '', 1);

    $GLOBALS['SITE_DB']->query_update('download_downloads', array('download_licence' => null), array('download_licence' => $id));

    log_it('DELETE_DOWNLOAD_LICENCE', strval($id), $myrow['l_title']);

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

/**
 * Log a file download, update the downloads counter and the download bandwidth counter.
 *
 * @param  AUTO_LINK $id The ID of the download being downloaded
 * @param  integer $size The size of the download (if zero, no bandwidth will be done - zero implies either an empty file, or a remote file that doesn't affect our bandwidth)
 * @param  boolean $got_before Whether the download has been downloaded before
 */
function log_download($id, $size, $got_before)
{
    // Log
    if (!$got_before) {
        $GLOBALS['SITE_DB']->query_insert('download_logging', array('id' => $id, 'member_id' => get_member(), 'ip' => get_ip_address(), 'date_and_time' => time()), false, true); // Suppress errors in case of race condition
    }

    // Update download count
    $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'download_downloads SET num_downloads=(num_downloads+1) WHERE id=' . strval($id), 1, null, true);

    // Update stats
    $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'values SET the_value=' . db_cast('(' . db_cast('the_value', 'INT') . '+1)', 'CHAR') . ' WHERE the_name=\'num_downloads_downloaded\'', 1, null, true);
    if ($size != 0) {
        $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'values SET the_value=' . db_cast('(' . db_cast('the_value', 'INT') . '+' . strval($size) . ')', 'CHAR') . ' WHERE the_name=\'download_bandwidth\'', 1, null, true);
    }
}

/**
 * Reorganise the download uploads.
 *
 * @param  ?array $where Limit reorganisation to rows matching this WHERE map (null: none)
 * @param  boolean $tolerate_errors Whether to tolerate missing files (false = give an error)
 */
function reorganise_uploads__download_categories($where = null, $tolerate_errors = false) // TODO: Change to array() in v11
{
    require_code('uploads2');
    reorganise_uploads('download_category', 'uploads/repimages', 'rep_image', $where, null, true, $tolerate_errors);
}

/**
 * Reorganise the download uploads.
 *
 * @param  ?array $where Limit reorganisation to rows matching this WHERE map (null: none)
 * @param  boolean $tolerate_errors Whether to tolerate missing files (false = give an error)
 */
function reorganise_uploads__downloads($where = null, $tolerate_errors = false) // TODO: Change to array() in v11
{
    require_code('uploads2');
    reorganise_uploads('download', 'uploads/downloads', 'url', $where, null, false, $tolerate_errors);
}
