<?php

/******************************************************************************
 *
 *	PROJECT: Flynax Classifieds Software
 *	VERSION: 4.8.0
 *	LICENSE: FL4HLD2A8696 - http://www.flynax.com/license-agreement.html
 *	PRODUCT: Real Estate Script
 *	DOMAIN: immofil.com
 *	FILE: PICTUREUPLOAD.PHP
 *
 *	The software is a commercial product delivered under single, non-exclusive,
 *	non-transferable license for one domain or IP address. Therefore distribution,
 *	sale or transfer of the file in whole or in part without permission of Flynax
 *	respective owners is considered to be illegal and breach of Flynax License End
 *	User Agreement.
 *
 *	You are not allowed to remove this information from the file without permission
 *	of Flynax respective owners.
 *
 *	Flynax Classifieds Software 2020 |  All copyrights reserved.
 *
 *	http://www.flynax.com/
 *
 ******************************************************************************/

namespace Flynax\Classes;

use Flynax\Utils\ListingMedia;

/**
 * @since 4.6.0
 */
class PictureUpload
{
    protected $error_messages = array();

    public $options;

    private $listingID;

    private $errors = [];

    public function __construct($listing_id = 0)
    {
        global $config, $reefless;

        $this->error_messages = array(
            1 => $GLOBALS['lang']['error_maxFileSize'], // max_file_size (php.ini) limit error
            2 => $GLOBALS['lang']['error_maxFileSize'], // max_file_size (class var) limit error
            // The phrases below are hardcoded because they are unlikely to be used
            3 => 'The uploaded file was only partially uploaded',
            4 => 'No file was uploaded',
            6 => 'Missing a temporary folder',
            7 => 'Failed to write file to disk',
            8 => 'A PHP extension stopped the file upload',
            'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini',
            'max_file_size' => 'File is too big',
            'min_file_size' => 'File is too small',
            'accept_file_types' => 'Filetype not allowed',
            'max_number_of_files' => 'Maximum number of files exceeded',
            'max_width' => 'Image exceeds maximum width',
            'min_width' => 'Image requires a minimum width',
            'max_height' => 'Image exceeds maximum height',
            'min_height' => 'Image requires a minimum height',
            'abort' => 'File upload aborted',
        );

        $reefless->loadClass('Actions');
        $reefless->loadClass('Resize');
        $reefless->loadClass('Crop');

        $this->options = array(
            'picture_file_types' => '/(\.|\/)(gif|jpe?g|png)$/i',
            'video_file_types' => '/(\.|\/)(mp4|flv|x-flv)$/i',
            // Defines which files (based on their names) are accepted for upload
            'accept_file_types' => '/(\.|\/)(gif|jpe?g|png|mp4|flv)$/i',
            // The php.ini settings upload_max_filesize and post_max_size
            // take precedence over the following max_file_size setting
            'max_file_size' => null,
            'min_file_size' => 1,
            'param_name' => 'files',
            // Image resolution restrictions:
            'max_width' => null,
            'max_height' => null,
            'min_width' => 1,
            'min_height' => 1,
            // Set the following option to false to enable resumable uploads:
            'discard_aborted_uploads' => true,
            // Set to true to rotate images based on EXIF meta data, if available:
            'orient_image' => true,
            // Set true to get name from listing title, false to use numeric hash as name
            'seo_picture_names' => true,
            // Adds a random number to aboid problem with cache to name
            'random_number_in_seo_name' => true,
            // File name template to use for picture names
            'file_name_template' => '{name_hash}-{index}_{postfix}',
            // Image version to create
            'image_versions' => array(
                'large' => array(
                    'prefix' => '_large',
                    'db_field' => 'Photo',
                    'max_width' => $config['pg_upload_large_width'] ? $config['pg_upload_large_width'] : 640,
                    'max_height' => $config['pg_upload_large_height'] ? $config['pg_upload_large_height'] : 480,
                    'watermark' => true,
                ),
                'Thumbnail' => array(
                    'prefix' => '',
                    'db_field' => 'Thumbnail',
                    'max_width' => $config['pg_upload_thumbnail_width'] ? $config['pg_upload_thumbnail_width'] : 120,
                    'max_height' => $config['pg_upload_thumbnail_height'] ? $config['pg_upload_thumbnail_height'] : 90,
                    'force_crop' => true,
                    'watermark' => false,
                ),
                'thumbnail_x2' => array(
                    'prefix' => '_x2',
                    'db_field' => 'Thumbnail_x2',
                    'max_width' => $config['pg_upload_thumbnail_width'] ? $config['pg_upload_thumbnail_width'] * 2 : 240,
                    'max_height' => $config['pg_upload_thumbnail_height'] ? $config['pg_upload_thumbnail_height'] * 2 : 180,
                    'force_crop' => true,
                    'watermark' => false,
                ),
            ),
        );

        // remove x2 thumbnails of related option disabled
        if (!$config['thumbnails_x2']) {
            unset($this->options['image_versions']['thumbnail_x2']);
        }

        $this->listingID = (int) ($listing_id ?: $_REQUEST['listing_id']);
        $this->accountInfo = $_SESSION['account'];

        $GLOBALS['rlHook']->load('phpUploadHandlerInit', $this->options);

        if (!$this->listingID) {
            die('PICTURE UPLOAD: No listing ID specified');
        }

        // Get listing owner ID
        // In case of ID = '-1' the process is also allowed because this means that singleStep
        // mode is eabled
        $owner_id = (int) $GLOBALS['rlDb']->getOne('Account_ID', "`ID` = {$this->listingID}", 'listings');

        if ($owner_id != '-1' && !$GLOBALS['rlAccount']->isLogin()) {
            die('PICTURE UPLOAD: User is not logged in');
        }

        if ($this->accountInfo['ID'] != $owner_id) {
            die('PICTURE UPLOAD: The listing with ' . $this->listingID . ' ID is not belong to logged in user');
        }
    }

    public function init()
    {
        switch ($_SERVER['REQUEST_METHOD']) {
            case 'GET':
                return $this->get();
                break;
            case 'POST':
                return $this->post();
                break;
            default:
                header('HTTP/1.1 405 Method Not Allowed');
        }
    }

    private function get()
    {
        $files = $GLOBALS['rlDb']->fetch(
            array('ID', 'Photo', 'Thumbnail', 'Description', 'Original', 'Type'),
            array('Listing_ID' => $this->listingID),
            'ORDER BY `Position`',
            null,
            'listing_photos');

        return array(
            'status' => 'OK',
            'results' => ListingMedia::prepareURL($files),
            'count' => count($files),
        );
    }

    private function post()
    {
        $GLOBALS['rlDb']->setTable('listing_photos');

        // get files data
        $upload = isset($_FILES[$this->options['param_name']])
        ? $_FILES[$this->options['param_name']]
        : null;

        // parse the Content-Disposition header
        $file_name = isset($_SERVER['HTTP_CONTENT_DISPOSITION'])
        ? rawurldecode(preg_replace('/(^[^"]+")|("$)/', '', $_SERVER['HTTP_CONTENT_DISPOSITION']))
        : null;

        // parse the Content-Description header
        $file_type = isset($_SERVER['HTTP_CONTENT_DESCRIPTION'])
        ? $_SERVER['HTTP_CONTENT_DESCRIPTION']
        : null;

        // parse the Content-Range header, which has the following form:
        // content-Range: bytes 0-524287/2000000
        $content_range = isset($_SERVER['HTTP_CONTENT_RANGE'])
        ? preg_split('/[^0-9]+/', $_SERVER['HTTP_CONTENT_RANGE'])
        : null;

        // get size
        $size = $content_range ? $content_range[3] : null;

        // upload pictures
        if ($upload && is_array($upload['tmp_name'])) {
            // create directory
            $this->createDirectory();

            // param_name is an array identifier like "files[]",
            // $_FILES is a multi-dimensional array:
            foreach ($upload['tmp_name'] as $index => $value) {
                $info[] = $this->uploadFile(
                    $upload['tmp_name'][$index],
                    $file_name ? $file_name : $upload['name'][$index],
                    $size ? $size : $upload['size'][$index],
                    $file_type ? $file_type : $upload['type'][$index],
                    $upload['error'][$index],
                    isset($_REQUEST['index']) ? $_REQUEST['index'] : $index,
                    $content_range
                );
            }
        } else {
            die('PICTURE UPLOAD: $_FILES is a one-dimensional array'); // TODO DEBUG
        }

        return array($this->options['param_name'] => $info);
    }

    /**
     * Build Media File SEO Name (picture or video)
     *
     * Build human readable media file name from the listing title
     *
     * @since 4.6.0
     *    
     * @param  string $postfix  - large, orig, x2 - picture postfix
     * @param  int    $position - Numeric photo/video file position
     * @return string           - Photo name
     */
    public function buildName($postfix, $position = 0, $extension = false)
    {
        global $lang, $rlListings;

        // Check if postfix starts with underscore _
        $postfix = substr($postfix, 0, 1) == '_' ? substr($postfix,1) : $postfix;

        if (!$this->options['seo_picture_names']) {
            $name_hash = time() . mt_rand();
            $index = '';
        } else {
            if (!$GLOBALS['rlListingTypes']) {
                $GLOBALS['reefless']->loadClass('ListingTypes');
            }
            $listing = $rlListings->getListing($this->listingID);

            if (!$GLOBALS['lang']) {
                $sql = "SELECT * FROM `" . RL_DBPREFIX . "lang_keys` ";
                $sql .="WHERE `Key` LIKE 'categories+name+%' AND `Code` = '{$GLOBALS['config']['lang']}'";
                $lang = $GLOBALS['rlDb']->getAll($sql, array('Key', 'Value'));
            }

            $listing_title = $rlListings->getListingTitle($listing['Category_ID'], $listing, $listing['Listing_type']);
            
            $name_hash = $GLOBALS['rlValid']->str2path($listing_title);
            
            // Remove trailing numbers to avoid issues with position
            $name_hash = preg_replace('/-[0-9]+$/', '', $name_hash);

            if ($this->options['random_number_in_seo_name']) {
                $index = mt_rand();
            } else {
                $index = $position > 0 ? $position : '';
            }
        }

        $file_name = str_replace(
            array('{name_hash}', '{postfix}', '{index}'),
            array($name_hash, $postfix, $index),
            $this->options['file_name_template']
        );

        $file_name = str_replace('-_', '_', $file_name);
        $file_name = trim($file_name, " \t\n\r\0\x0B-_");

        $check_file = $this->options['upload_dir'] . $file_name . '.' . $extension;
        
        if (is_file($check_file)) {
            return $this->buildName($postfix, ++$position, $extension);
        }

        return $file_name;
    }

    /**
     * Update Media (picture and video) Names 
     *
     * Update names when listing title changed
     */
    public function updateNames()
    {
        global $rlDebug;

        $sql = "SELECT * FROM `" . RL_DBPREFIX . "listing_photos` ";
        $sql .="WHERE `Listing_ID` = {$this->listingID}";
        $listing_photos = $GLOBALS['rlDb']->getAll($sql);

        $update = array();
        foreach ($listing_photos as $key => $photo) {
            $upd = array();

            $fileInfo = pathinfo($photo['Original']);

            $this->options['upload_dir'] = RL_FILES . $fileInfo['dirname'] . '/';

            if ($photo['Type'] == 'picture') {
                $options = $this->options['image_versions'];
            } else {
                $options = array();
            }

            $orig_option = array(
                'prefix' => '_orig',
                'db_field' => 'Original'
                );
            array_push($options, $orig_option);

            foreach ($options as &$option) {
                $db_field = $option['db_field'];
            
                $upd['fields'][$db_field] = $fileInfo['dirname'] . '/';
                $upd['fields'][$db_field] .= $this->buildName($option['prefix'], $photo['Position'], $fileInfo['extension']);
                $upd['fields'][$db_field] .= '.' . $fileInfo['extension'];

                if (is_file(RL_FILES . $photo[$db_field])) {
                    if (!rename(RL_FILES . $photo[$db_field], RL_FILES . $upd['fields'][$db_field])) {
                        $rlDebug->logger('Media file rename failed on file - ' . $photo[$db_field]);
                    } else {
                        $upd['where']['ID'] = $photo['ID'];
                    }
                } else {
                    $rlDebug->logger('Media file copying error; no file found - ' . $photo[$db_field]);
                }
            }            

            if ($upd['where']['ID']) {
                $GLOBALS['rlActions']->updateOne($upd, 'listing_photos');
            }
        }
        
        // update media data
        ListingMedia::updateMediaData($this->listingID);

        return true;
    }

    private function uploadFile($uploaded_file, $name, $size, $type, $error, $index = null, $content_range = null)
    {
        global $config;

        // upload file        
        $extension = pathinfo($name, PATHINFO_EXTENSION);        
        
        $file_name = $this->buildName('orig', $index, $extension);
        
        $file = array();
        $file['name'] = $file_name . '.' . $extension;
        $file['size'] = $this->fixTntegerOverflow(intval($size));
        $file['type'] = $type;

        if ($this->validate($uploaded_file, $file, $error, $index)) {
            if ((bool) preg_match($this->options['picture_file_types'], $type)) {
                $media_type = 'picture';
            } elseif ((bool) preg_match($this->options['video_file_types'], $type)) {
                $media_type = 'video';
            }

            $file_path = $this->options['upload_dir'] . $file['name'];
            $append_file = $content_range && is_file($file_path) && $file['size'] > $this->getFileSize($file_path);

            if ($uploaded_file && is_uploaded_file($uploaded_file)) {
                // multipart/formdata uploads (POST method uploads)
                if ($append_file) {
                    file_put_contents(
                        $file_path,
                        fopen($uploaded_file, 'r'),
                        FILE_APPEND
                    );
                } else {
                    move_uploaded_file($uploaded_file, $file_path);
                }
            } else {
                // Non-multipart uploads (PUT method support)
                file_put_contents(
                    $file_path,
                    fopen('php://input', 'r'),
                    $append_file ? FILE_APPEND : 0
                );
            }

            // get file orientation
            if (function_exists('exif_read_data')) {
                $exif = exif_read_data($file_path);
                $orientation = $exif['Orientation'];
            }

             // insert to DB
            $insert = array(
                'Listing_ID'  => $this->listingID,
                'Position'    => $index + 1,
                'Original'    => $this->options['dir_name'] . $file['name'],
                'Description' => '',
                'Type'        => $media_type,
                'Status'      => 'active',
            );

            // create image versions
            if ($media_type == 'picture') {
                $file_size = $this->getFileSize($file_path, $append_file);

                if ($file_size === $file['size']) {
                    if ($this->options['orient_image']) {
                        $this->orientImage($file_path, $orientation);
                    }

                    foreach ($this->options['image_versions'] as $version => $options) {
                        $file_name = $this->buildName($options['prefix'], $index, $extension);
                        $new_file_name = $file_name . '.' . $extension;

                        if ($this->createScaledImage($file['name'], $new_file_name, $options, $version, $orientation)) {
                            $insert[$options['db_field']] = $file[$options['db_field']] = $this->options['dir_name'] . $new_file_name;
                        }
                    }
                } elseif (!$content_range && $this->options['discard_aborted_uploads']) {
                    unlink($file_path);
                    $file['error'] = 'abort';
                }
            }

            $GLOBALS['rlHook']->load('phpUploadBeforeSaveData', $insert, $this->options['dir_name'], $file);

            // insert new photo
            $GLOBALS['rlActions']->insertOne($insert, 'listing_photos');

            // send data
            $file['ID'] = $GLOBALS['rlDb']->insertID();
            $file['primary'] = 0;
            $file['description'] = '';
            $file['Type'] = $media_type;
            $file['Original'] = $this->options['dir_name'] . $file['name'];

            // update media data
            ListingMedia::updateMediaData($this->listingID);

            // prepare URLs
            ListingMedia::prepareURL($file, true);
        }

        return $file;
    }

    private function createDirectory()
    {
        if ($existing_photo = $GLOBALS['rlDb']->getOne('Photo', "`Listing_ID` = '{$this->listingID}'")) {
            $exp_dir = explode('/', $existing_photo);

            if (count($exp_dir) > 1) {
                array_pop($exp_dir);

                $dir = RL_FILES . implode(RL_DS, $exp_dir) . RL_DS;
                $dir_name = implode('/', $exp_dir) . '/';
            }
        }

        if (!$dir) {
            $dir = RL_FILES . date('m-Y') . RL_DS . 'ad' . $this->listingID . RL_DS;
            $dir_name = date('m-Y') . '/ad' . $this->listingID . '/';
        }

        $url = RL_FILES_URL . $dir_name;

        $GLOBALS['rlHook']->load('phpUploadPost', $dir, $dir_name, $url);

        $GLOBALS['reefless']->rlMkdir($dir);

        // assign data
        $this->options['upload_dir'] = $dir;
        $this->options['upload_url'] = $url;
        $this->options['dir_name'] = $dir_name;
    }

    // Fix for overflowing signed 32 bit integers,
    // works for sizes up to 2^32-1 bytes (4 GiB - 1)
    private function fixTntegerOverflow($size)
    {
        if ($size < 0) {
            $size += 2.0 * (PHP_INT_MAX + 1);
        }
        return $size;
    }

    private function validate($uploaded_file, &$file, $error, $index)
    {
        if ($error) {
            $file['error'] = $this->getRrrorMessage($error);
            return false;
        }

        $content_length = $this->fixTntegerOverflow(intval($_SERVER['CONTENT_LENGTH']));
        if ($content_length > $this->getConfigBytes(ini_get('post_max_size'))) {
            $file['error'] = $this->getRrrorMessage('post_max_size');
            return false;
        }
        if (!preg_match($this->options['accept_file_types'], $file['name'])) {
            $file['error'] = $this->getRrrorMessage('accept_file_types');
            return false;
        }
        if ($uploaded_file && is_uploaded_file($uploaded_file)) {
            $file_size = $this->getFileSize($uploaded_file);
        } else {
            $file_size = $content_length;
        }
        if ($this->options['max_file_size']
            && ($file_size > $this->options['max_file_size'] || $file['size'] > $this->options['max_file_size'])
        ) {
            $file['error'] = $this->getRrrorMessage('max_file_size');
            return false;
        }
        if ($this->options['min_file_size'] &&
            $file_size < $this->options['min_file_size']) {
            $file['error'] = $this->getRrrorMessage('min_file_size');
            return false;
        }

        list($img_width, $img_height) = @getimagesize($uploaded_file);
        if (is_int($img_width)) {
            if ($this->options['min_width'] && $img_width < $this->options['min_width']) {
                $file['error'] = $this->getRrrorMessage('min_width');
                return false;
            }
            if ($this->options['min_height'] && $img_height < $this->options['min_height']) {
                $file['error'] = $this->getRrrorMessage('min_height');
                return false;
            }
        }
        return true;
    }

    private function orientImage($file_path, $orientation)
    {
        if ($orientation <= 0) {
            return false;
        }

        $image = @imagecreatefromjpeg($file_path);
        switch ($orientation) {
            case 3:
                $image = @imagerotate($image, 180, 0);
                break;
            case 6:
                $image = @imagerotate($image, 270, 0);
                break;
            case 8:
                $image = @imagerotate($image, 90, 0);
                break;
            default:
                return false;
        }

        $success = imagejpeg($image, $file_path);
        @imagedestroy($image);

        return $success;
    }

    private function createScaledImage($file_name, $new_file_name, $options, $version, $orientation = 0)
    {
        global $rlCrop, $config;

        $file_path = $this->options['upload_dir'] . $file_name;
        $new_file_path = $this->options['upload_dir'] . $new_file_name;

        $GLOBALS['rlHook']->load('phpUploadScaledImage', $file_name, $new_file_name, $this, $options, $version);

        if ($cropped = $config['img_crop_module'] || $options['force_crop']) {
            $rlCrop->loadImage($file_path);
            $rlCrop->cropBySize($options['max_width'], $options['max_height'], ccCENTRE);
            $rlCrop->saveImage($new_file_path, $config['img_quality']);
            $rlCrop->flushImages();
        }

        $GLOBALS['rlResize']->resize(
            $cropped ? $new_file_path : $file_path,
            $new_file_path,
            'C',
            array($options['max_width'], $options['max_height']),
            $options['force_crop'],
            $options['watermark']
        );

        return true;
    }

    private function getRrrorMessage($error)
    {
        return array_key_exists($error, $this->error_messages) ? $this->error_messages[$error] : $error;
    }

    private function getConfigBytes($val)
    {
        $val = trim($val);
        $last = strtolower($val[strlen($val) - 1]);

        switch ($last) {
            case 'g':
                $val *= 1024;
            case 'm':
                $val *= 1024;
            case 'k':
                $val *= 1024;
        }

        return $this->fixTntegerOverflow($val);
    }

    private function getFileSize($file_path, $clear_stat_cache = false)
    {
        if ($clear_stat_cache) {
            clearstatcache();
        }

        return $this->fixTntegerOverflow(filesize($file_path));
    }
}
