<?php

/*
 *   $Id$
 *
 *   AbanteCart, Ideal OpenSource Ecommerce Solution
 *   http://www.AbanteCart.com
 *
 *   Copyright © 2011-2025 Belavier Commerce LLC
 *
 *   This source file is subject to Open Software License (OSL 3.0)
 *   License details are bundled with this package in the file LICENSE.txt.
 *   It is also available at this URL:
 *   <http://www.opensource.org/licenses/OSL-3.0>
 *
 *  UPGRADE NOTE:
 *    Do not edit or add to this file if you wish to upgrade AbanteCart to newer
 *    versions in the future. If you wish to customize AbanteCart for your
 *    needs, please refer to http://www.AbanteCart.com for more information.
 */
if (!defined('DIR_CORE')) {
    header('Location: static_pages/');
}

/**
 * Class ADownload - class to manage downloads
 *
 * @property ADB $db
 * @property ALanguageManager $language
 * @property ACustomer $customer
 * @property AConfig $config
 * @property ALog $log
 * @property ALoader $load
 * @property ExtensionsAPI $extensions
 * @property ARequest $request
 * @property AOrderStatus $order_status
 */
class ADownload
{

    public $errors = [];

    /**
     * @var registry - access to application registry
     */
    protected $registry;

    public function __construct()
    {
        $this->registry = Registry::getInstance();
        $this->errors = [];
    }

    /**
     * @param  $key - key to load data from registry
     *
     * @return mixed  - data from registry
     */
    public function __get($key)
    {
        return $this->registry->get($key);
    }

    public function getDownloadsBeforeOrder($product_id, ?int $language_id = 0)
    {
        if (!(int)$product_id) {
            return [];
        }
        if (!$language_id) {
            if (IS_ADMIN === true) {
                $language_id = $this->language->getContentLanguageID();
            } else {
                $language_id = $this->language->getLanguageID();
            }
        }

        $result = $this->db->query(
            "SELECT d.*, dd.*, ptd.*
            FROM " . $this->db->table('downloads') . " d
            RIGHT JOIN " . $this->db->table('products_to_downloads') . " ptd 
                ON ptd.download_id = d.download_id
            LEFT JOIN " . $this->db->table('download_descriptions') . " dd 
                ON (d.download_id = dd.download_id AND dd.language_id = '" . $language_id . "')
            WHERE ptd.product_id='" . (int)$product_id . "'
                AND d.activate='before_order'
                AND d.status>0"
        );
        return $result->rows;
    }

    public function getDownloadInfo($download_id)
    {
        if (!(int)$download_id) {
            return [];
        }
        if (IS_ADMIN === true) {
            $language_id = $this->language->getContentLanguageID();
        } else {
            $language_id = $this->language->getLanguageID();
        }

        $result = $this->db->query(
            "SELECT dd.*, d.*
                                    FROM " . $this->db->table('downloads') . " d
                                    LEFT JOIN " . $this->db->table('download_descriptions') . " dd
                                        ON (d.download_id = dd.download_id AND dd.language_id = '" . $language_id . "')
                                    WHERE d.download_id='" . (int)$download_id . "'"
        );
        return $result->row;
    }

    /**
     * Method returns order download info selected by order_download_id
     *
     * @param int $order_download_id
     * @param int|string $language_id (optional)
     *
     * @return array
     * @throws AException
     */
    public function getOrderDownloadInfo($order_download_id, $language_id = '')
    {
        if (!(int)$order_download_id) {
            return [];
        }
        if (!$language_id) {
            if (IS_ADMIN === true) {
                $language_id = $this->language->getContentLanguageID();
            } else {
                $language_id = $this->language->getLanguageID();
            }
        }

        $result = $this->db->query(
            "SELECT dd.*, d.*, od.*
                                    FROM " . $this->db->table('order_downloads') . " od
                                    LEFT JOIN " . $this->db->table('downloads') . " d 
                                        ON od.download_id = d.download_id
                                    LEFT JOIN " . $this->db->table('download_descriptions') . " dd 
                                        ON d.download_id = dd.download_id AND dd.language_id = '" . $language_id . "'
                                    WHERE od.order_download_id='" . (int)$order_download_id . "'"
        );
        return $result->row;
    }

    /**
     * Method returns list of downloads that will be append to order
     *
     * @param int $product_id
     *
     * @return array
     * @throws AException
     */
    public function getProductOrderDownloads($product_id)
    {
        if (!(int)$product_id) {
            return [];
        }
        $query = $this->db->query(
            "SELECT dd.*, d.*, p2d.*
                FROM " . $this->db->table("products_to_downloads") . " p2d
                INNER JOIN " . $this->db->table("downloads") . " d 
                    ON (p2d.download_id = d.download_id)
                LEFT JOIN " . $this->db->table("download_descriptions") . " dd
                    ON (d.download_id = dd.download_id
                        AND dd.language_id = '" . (int)$this->config->get('storefront_language_id') . "')
                WHERE p2d.product_id = '" . (int)$product_id . "'
                    AND d.status=1 AND d.activate<>'before_order' "
        );
        return $query->rows;
    }

    /**
     * Method to add/update download in order
     * Method returns list of downloads that will be append to order
     *
     * @param (int) $order_product_id
     * @param (int) $order_id
     * @param array $download
     *
     * @return array (added/updated ids)
     * @throws AException
     */
    public function addUpdateOrderDownload($order_product_id, $order_id, $download = [])
    {
        if (!(int)$order_product_id || !(int)$order_id || !(int)$download['download_id']) {
            return [];
        }

        if ($download['activate'] != 'order_status') {
            $download['activate_order_status_id'] = 0;
        }
        if ($download['activate'] == 'before_order') {
            $download['max_downloads'] = '';
            $download['expire_days'] = '';
        }

        //check if we have download yet
        $check = $this->db->query(
            "SELECT od.order_download_id
                                    FROM " . $this->db->table('order_downloads') . " od
                                    WHERE od.order_id='" . (int)$order_id . "'
                                            AND od.order_product_id='" . (int)$order_product_id . "' 
                                            AND od.download_id='" . (int)$download['download_id'] . "'"
        );
        if ($check->num_rows) {
            //need to update
            $return = [];
            //loop cause we can have multiple records
            foreach ($check->rows as $row) {
                $return[] = $this->updateProductDownloadToOrder($row['order_download_id'], $download);
            }
            return $return;
        } else {
            //create new
            return [$this->addProductDownloadToOrder($order_product_id, $order_id, $download)];
        }
    }

    /**
     * Method to update download to the order
     *
     * @param (int) $order_download_id
     * @param array $download
     *
     * @return int (added id)
     * @throws AException
     */
    public function updateProductDownloadToOrder($order_download_id, $download = [])
    {
        if (!(int)$order_download_id || empty($download)) {
            return false;
        }
        if ((int)$download['expire_days']) {
            $expire = "(NOW() + INTERVAL " . (int)$download['expire_days'] . " DAY)";
        } else {
            $expire = 'NULL';
        }
        $activateOrderStatusId = is_serialized($download['activate_order_status_id'])
            ? $download['activate_order_status_id']
            : serialize($download['activate_order_status_id']);
        $this->db->query(
            "UPDATE " . $this->db->table("order_downloads") . "
                            SET name = '" . $this->db->escape($download['name']) . "',
                                filename = '" . $this->db->escape($download['filename']) . "',
                                mask = '" . $this->db->escape($download['mask']) . "',
                                remaining_count = " . ((int)$download['max_downloads'] ? "'"
                . (int)$download['max_downloads'] . "'" : 'NULL') . ",
                                status = '" . (int)$download['status'] . "',
                                activate_order_status_id = '" . $this->db->escape($activateOrderStatusId) . "',
                                expire_date = " . $expire . ",
                                attributes_data = '" . $this->db->escape($download['attributes_data']) . "',
                                date_modified = NOW()
                            WHERE order_download_id = '" . (int)$order_download_id . "'"
        );

        return $order_download_id;
    }

    /**
     * Method to add download to the order
     *
     * @param (int) $order_product_id
     * @param (int) $order_id
     * @param array $download
     *
     * @return int (added id)
     * @throws AException
     */
    public function addProductDownloadToOrder($order_product_id, $order_id, $download = [])
    {
        if (!(int)$order_product_id || !(int)$order_id) {
            return false;
        }
        if ((int)$download['expire_days']) {
            $expire = "(NOW() + INTERVAL " . (int)$download['expire_days'] . " DAY)";
        } else {
            $expire = 'NULL';
        }
        $activateOrderStatusId = is_serialized($download['activate_order_status_id'])
            ? $download['activate_order_status_id']
            : serialize($download['activate_order_status_id']);
        $this->db->query(
            "INSERT INTO " . $this->db->table("order_downloads") . "
                            SET order_id = '" . (int)$order_id . "',
                                order_product_id = '" . (int)$order_product_id . "',
                                download_id = '" . (int)$download['download_id'] . "',
                                name = '" . $this->db->escape($download['name']) . "',
                                filename = '" . $this->db->escape($download['filename']) . "',
                                mask = '" . $this->db->escape($download['mask']) . "',
                                remaining_count = " . ((int)$download['max_downloads'] ? "'"
                . (int)$download['max_downloads'] . "'" : 'NULL') . ",
                                status = '" . (int)$download['status'] . "',
                                activate_order_status_id = '" . $this->db->escape($activateOrderStatusId) . "',
                                expire_date = " . $expire . ",
                                attributes_data = '" . $this->db->escape($download['attributes_data']) . "',
                                date_modified = NOW(),
                                date_added = NOW()"
        );

        return $this->db->getLastId();
    }

    /**
     * @param int $download_id
     * @param string $mode - can be "full" - all download attributes (with empty values too),
     *                     "to_customer" - download attributes with values that allowed to display for customers,
     *                     "to_display"  - all download attributes with values
     *
     * @return array
     * @throws AException
     */
    public function getDownloadAttributesValues($download_id, $mode = 'full')
    {
        if (!(int)$download_id) {
            return [];
        }
        $output = [];
        $attr = new AAttribute('download_attribute');
        $attributes = $attr->getAttributes();

        $ids = [];
        foreach ($attributes as &$attribute) {
            if ($mode == 'to_customer') {
                $attribute['settings'] = unserialize($attribute['settings']);
                if (!$attribute['settings']['show_to_customer']) {
                    continue;
                }
            }
            $ids[] = (int)$attribute['attribute_id'];
            $attribute['attribute_values'] = $attr->getAttributeValues($attribute['attribute_id']);
        }
        unset($attribute);

        if ($ids) {
            $result = $this->db->query(
                "SELECT dav.attribute_id, dav.attribute_value_ids as value
                                          FROM " . $this->db->table('download_attribute_values') . " dav
                                          LEFT JOIN " . $this->db->table('global_attributes') . " ga
                                                ON ga.attribute_id = dav.attribute_id
                                          WHERE dav.attribute_id IN (" . implode(',', $ids) . ") AND dav.download_id = '"
                . $download_id . "'
                                          ORDER BY ga.sort_order ASC"
            );

            $attributes_with_options = HtmlElementFactory::getElementsWithOptions();
            foreach ($result->rows as $row) {
                if (!in_array($row['attribute_id'], $ids)) {
                    continue;
                }
                $row['value'] = unserialize($row['value']);
                if (in_array($attributes[$row['attribute_id']]['element_type'], $attributes_with_options)) {
                    foreach ($attributes[$row['attribute_id']]['attribute_values'] as $values) {
                        if ((is_array($row['value']) && in_array($values['attribute_value_id'], $row['value']))) {
                            // do not include empty value for display for customer
                            if (!$row['value'] && in_array($mode, ['to_customer', 'to_display'])) {
                                continue 1;
                            }
                            $output[$attributes[$row['attribute_id']]['name']][] = $values['value'];
                        } elseif (!is_array($row['value']) && $values['attribute_value_id'] == $row['value']) {
                            // do not include empty value for display for customer
                            if (!$row['value'] && in_array($mode, ['to_customer', 'to_display'])) {
                                continue 1;
                            }
                            $output[$attributes[$row['attribute_id']]['name']] = $values['value'];
                        }
                    }
                } else {
                    // do not include empty value for display for customer or admin display
                    if (!has_value($row['value']) && in_array($mode, ['to_customer', 'to_display'])) {
                        continue;
                    }
                    $output[$attributes[$row['attribute_id']]['name']] = $row['value'];
                    // for checkbox value show text yes or no
                    if ($attributes[$row['attribute_id']]['element_type'] == 'C') {
                        $output[$attributes[$row['attribute_id']]['name']] =
                            $row['value'] ? $this->language->get('text_yes') : $this->language->get('text_no');
                    }
                }
            }
        }
        return $output;
    }

    public function getDownloadAttributesValuesForCustomer($download_id)
    {
        return $this->getDownloadAttributesValues($download_id, 'to_customer');
    }

    public function getDownloadAttributesValuesForDisplay($download_id)
    {
        return $this->getDownloadAttributesValues($download_id, 'to_display');
    }

    public function sendDownload($download_info = [])
    {
        // do checks
        if (!$download_info || !$this->isFileAvailable($download_info['filename'])) {
            $error_text = 'Unable to download file ' . DIR_RESOURCE . $download_info['filename']
                . '! File is unavailable. Please check permissions.';
            $err = new AError($error_text);
            $err->toDebug()->toLog();
            return false;
        }
        if ($download_info['remaining_count'] != '' && $download_info['remaining_count'] < 1) {
            return false;
        }
        if ($download_info['expire_date'] != '' && dateISO2Int($download_info['expire_date']) < time()) {
            return false;
        }

        $file = DIR_RESOURCE . str_replace('/', DS, $download_info['filename']);
        if (is_numeric($download_info['filename'])) {
            $rl = new AResource('download');
            $resource = $rl->getResource($download_info['filename']);
            if ($resource && $resource['resource_path']) {
                $file = DIR_RESOURCE . $rl->getTypeDir() . str_replace('/', DS, $resource['resource_path']);
            }
        }
        $mask = basename($download_info['mask']);
        $mask = mb_convert_encoding($mask, 'UTF-8', 'UTF-8');
        $mask = preg_replace('/[^0-9A-z_.\-]/', '', $mask);
        //is mask filename contains only non-latin chars
        if (pathinfo($mask, PATHINFO_FILENAME) == '' || !$mask) {
            $mask = basename($file);
        }

        $mime = getMimeType($file);
        $encoding = 'binary';
        if (!headers_sent()) {
            if (file_exists($file)) {
                $file_handler = fopen($file, "rb");
                $filesize = filesize($file);
                header('Pragma: public');
                header('Expires: 0');
                header('Content-Description: File Transfer');
                header('Content-Type: ' . $mime);
                header('Content-Transfer-Encoding: ' . $encoding);
                header('Content-Disposition: attachment; filename=' . $mask);
                header('Content-Length: ' . $filesize);
                ob_end_clean();
                $bytes_sent = 0;
                $order_download_history_id = 'NULL';
                while (!feof($file_handler)) {// if we haven't got to the End Of File
                    print(fread($file_handler, 1024 * 8));//read 8k from the file and send to the user
                    flush();//force the previous line to send its info
                    if (connection_status() != 0) {//check the connection, if it has ended...
                        fclose($file_handler);
                        exit();
                    } else {
                        $bytes_sent += 1024 * 8;
                        $prc = round($bytes_sent * 100 / $filesize, 0);
                        $prc = min($prc, 100);
                        //do insert or update
                        $sql = "INSERT INTO " . $this->db->table('order_downloads_history') . "
                                (order_download_history_id,
                                 order_download_id,
                                 order_id,
                                 order_product_id,
                                 filename,
                                 mask,
                                 download_id,
                                 download_percent,
                                 `time`)
                        VALUES (" . $order_download_history_id . ",
                                '" . (int)$download_info['order_download_id'] . "',
                                '" . (int)$download_info['order_id'] . "',
                                '" . (int)$download_info['order_product_id'] . "',
                                '" . $this->db->escape($download_info['filename']) . "',
                                '" . $this->db->escape($download_info['mask']) . "',
                                '" . $download_info['download_id'] . "',
                                '" . $prc . "',
                                NOW())
                        ON DUPLICATE KEY UPDATE 
                         download_percent = '" . $prc . "'";

                        $this->db->query($sql);
                        if ($order_download_history_id == 'NULL') {
                            $order_download_history_id = $this->db->getLastId();
                        }
                    }
                }
                fclose($file_handler);
            } else {
                throw new AException(AC_ERR_LOAD, 'Error: Could not find file ' . $file . '!');
            }
        } else {
            exit('Error: Headers already sent out!');
        }

        if ($download_info['remaining_count'] != '') { //if count is not NULL (unlimited)
            $this->updateRemaining($this->request->get['order_download_id']);
        }

        //init controller data
        $this->extensions->hk_UpdateData($this, __FUNCTION__);
        exit;
    }

    /**
     * @param $order_download_id
     *
     * @throws AException
     */
    public function updateRemaining($order_download_id)
    {
        $this->db->query(
            "UPDATE " . $this->db->table("order_downloads") . "
            SET remaining_count = (remaining_count - 1)
            WHERE order_download_id = '" . (int)$order_download_id . "'"
        );
    }

    /**
     * @param int $start
     * @param int $limit
     *
     * @return array
     * @throws AException
     */
    public function getCustomerDownloads($start = 0, $limit = 0)
    {
        $customer_id = (int)$this->customer->getId();
        if (!$customer_id) {
            return [];
        }
        $start = $start < 0 ? 0 : (int)$start;

        $sql = "SELECT o.order_id,
                      o.order_status_id,
                      od.download_id,
                      od.status,
                      od.date_added,
                      od.order_download_id,
                      d.activate,
                      od.activate_order_status_id,
                      od.name,
                      od.filename,
                      od.mask,
                      od.remaining_count,
                      od.expire_date,
                      op.product_id
               FROM " . $this->db->table("order_downloads") . " od
               LEFT JOIN " . $this->db->table("orders") . " o 
                    ON (od.order_id = o.order_id)
               LEFT JOIN " . $this->db->table("downloads") . " d 
                    ON (d.download_id = od.download_id)
               LEFT JOIN " . $this->db->table("order_products") . " op 
                    ON (op.order_product_id = od.order_product_id)
               WHERE o.customer_id = '" . $customer_id . "'
               ORDER BY  o.date_added DESC, od.sort_order ASC ";
        if ($limit) {
            $sql .= "LIMIT " . (int)$start . "," . (int)$limit;
        }

        $query = $this->db->query($sql);
        $downloads = [];
        foreach ($query->rows as $download_info) {
            $downloads[$download_info['order_download_id']] = $download_info;
        }
        return $downloads;
    }

    /**
     * @param int $order_id
     * @param int $customer_id
     *
     * @return array
     * @throws AException
     */
    public function getCustomerOrderDownloads($order_id, $customer_id)
    {
        $customer_id = (int)$customer_id;
        $order_id = (int)$order_id;
        if (!$order_id) {
            return [];
        }
        $sql = "SELECT o.order_id,
                      o.order_status_id,
                      od.download_id,
                      od.status,
                      od.date_added,
                      od.order_download_id,
                      d.activate,
                      od.activate_order_status_id,
                      od.name,
                      od.filename,
                      od.mask,
                      od.remaining_count,
                      od.expire_date,
                      op.product_id,
                      o.email
               FROM " . $this->db->table("order_downloads") . " od
               INNER JOIN " . $this->db->table("orders") . " o 
                    ON (od.order_id = o.order_id)
               LEFT JOIN " . $this->db->table("downloads") . " d 
                    ON (d.download_id = od.download_id)
               LEFT JOIN " . $this->db->table("order_products") . " op 
                    ON (op.order_product_id = od.order_product_id)
               WHERE o.order_id = '" . $order_id . "'
               " . ($customer_id ? " AND o.customer_id = '" . $customer_id . "'" : "") . "
               ORDER BY  o.date_added DESC, od.sort_order ASC ";

        $query = $this->db->query($sql);
        $downloads = [];
        foreach ($query->rows as $download_info) {
            $downloads[$download_info['order_download_id']] = $download_info;
        }
        return $downloads;
    }

    /**
     * @return mixed
     * @throws AException
     */
    public function getTotalDownloads()
    {
        return sizeof($this->getCustomerDownloads());
    }

    /**
     * @param int $order_id
     * @param int $customer_id
     *
     * @return int
     * @throws AException
     */
    public function getTotalOrderDownloads($order_id, $customer_id)
    {
        return sizeof($this->getCustomerOrderDownloads($order_id, $customer_id));
    }

    /**
     * @param int $order_id
     *
     * @return int
     * @throws AException
     */
    public function OrderHasDownloads($order_id)
    {
        $customer = (int)$this->customer->getId();
        return $this->getTotalOrderDownloads($order_id, $customer);
    }

    /**
     * @param string $resource_path
     *
     * @return bool
     * @throws AException
     */
    public function isFileAvailable($resource_path = '')
    {
        if (!$resource_path) {
            return false;
        }
        if (is_numeric($resource_path)) {
            $rl = new AResource('download');
            $resource = $rl->getResource($resource_path);
            if ($resource && $resource['resource_path']) {
                $resource_path = $rl->getTypeDir() . str_replace('/', DS, $resource['resource_path']);
            }
        }

        if (!is_file(DIR_RESOURCE . $resource_path) || !is_readable(DIR_RESOURCE . $resource_path)) {
            return false;
        }

        if (!filesize(DIR_RESOURCE . $resource_path)) {
            return false;
        }

        return true;
    }

    public function getTextStatusForOrderDownload($download_info)
    {
        $this->load->language('account/download');
        $text_status = '';

        if ($download_info['status'] == 0) {
            $text_status = $this->language->get('text_pending');
        } elseif (dateISO2Int($download_info['expire_date']) < time()) {
            $text_status = $this->language->get('text_expired');
        } elseif ($download_info['remaining_count'] == '0') {
            $text_status = $this->language->get('text_reached_limit');
        }
        $activateStatuses = unserialize($download_info['activate_order_status_id']);
        if ($activateStatuses) {
            if (!in_array((int)$download_info['order_status_id'], (array)$activateStatuses)) {
                $text_status = $this->language->get('text_pending');
            }
        }
        //2. check is file exists
        $download_info['filename'] = trim($download_info['filename']);
        if (!$this->isFileAvailable($download_info['filename'])) {
            $err = new AError(
                'Error: file "' . $download_info['filename'] . '" (download_id = ' . $download_info['order_id'] . ') of order #'
                . $download_info['order_id'] . ' is unavailable for download!'
            );
            $err->toLog()->toDebug();
            $text_status = $this->language->get('text_unavailable');
        }

        return $text_status;
    }
}
