2.3.x-and-above/upload/catalog/controller/extension/module/facebook_business.php [39:555]:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                        return false;
                    }
                }
            } else {
                return false;
            }

            $end_time = time();
            $feed_gen_time = $end_time - $start_time;
        
            $this->estimateFeedGenerationTimeWithDecay($feed_gen_time);
      
            // genFeedPing return time estimation only
            if (isset($this->request->get['from']) && $this->request->get['from'] == 'genFeedPing') {
                return;
            }
      
            $this->sendFileResponse($product_feed_path);
        } catch (Exception $e) {
            $this->response->addHeader('Content-type: text');
            $this->response->setOutput('There was a problem generating your feed: %s', $e->getMessage());
        }
    }

    public function genFeedNow() {
        $this->genFeed(true);
    }

    public function genFeedPing() {
        $this->load->model('catalog/product');
        $this->load->model('extension/module/facebook_business');

        $time = $this->estimateFeedGenerationTime();
        $this->response->addHeader('Content-type: text/plain');
        $this->response->setOutput(round($time));

        // This will call the genAction method above in an async request
        // so that we can still return a response from the ping action.
        try {
            $url = HTTP_SERVER.'index.php?route=extension/module/facebook_business/genFeed&from=genFeedPing';
            $curl = curl_init($url);
            curl_setopt($curl, CURLOPT_TIMEOUT, 1);
            curl_exec($curl);
            curl_close($curl);
        } catch (Exception $e) {
            // We expect the result to time out.
        }
    }

    public function appStorePing() {
        $json = array();

        $this->load->model('extension/module/facebook_business');
        $json['version'] = $this->model_extension_module_facebook_business->getPluginVersion();
        $json['store_url'] = $this->url->link('extension/module/facebook_business', '', true);

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($json));
    }

    public function appStoreConnect() {
        $json = array();

        $this->load->model('extension/module/facebook_business');

        if (($this->request->server['REQUEST_METHOD'] == 'POST') && isset($this->request->post['store_url']) && isset($this->request->post['username']) && isset($this->request->post['password'])) {
            if (strpos(HTTPS_SERVER, $this->request->post['store_url']) !== false && $this->model_extension_module_facebook_business->isVerifiedAdminUser($this->request->post['username'], $this->request->post['password'])) {
                if ($this->model_extension_module_facebook_business->isExtensionInstalled()) {
                    if ($this->config->get('facebook_fbe_v2_installed')) {
                        $json['error'] = 'Your store is already connected to a Facebook Business Page.';
                    } elseif (!$this->model_extension_module_facebook_business->isVerifiedAdminUser($this->request->post['username'], $this->request->post['password'], 'extension/module/facebook_business')) {
                        $json['error'] = 'OpenCart Admin user does not have the correct permissions to perform this action!';
                    }
                } elseif (!$this->model_extension_module_facebook_business->isVerifiedAdminUser($this->request->post['username'], $this->request->post['password'], 'extension/extension/module', true)) {
                    $json['error'] = 'OpenCart Admin user does not have the correct permissions to perform this action!';
                }

                if (!$json) {
                    $json['success'] = true;
                    $json['fbe_params'] = array(
                        'external_business_id' => HTTPS_SERVER,
                        'business_name'        => $this->config->get('config_name') ? $this->config->get('config_name') : 'My Business',
                        'feed_url'             => $this->url->link('extension/module/facebook_business/genFeed', '', true),
                        'feed_ping_url'        => $this->url->link('extension/module/facebook_business/genFeedPing', '', true),
                        'timezone'             => date_default_timezone_get(),
                        'currency'             => strtoupper(addslashes($this->config->get('config_currency'))),
                        'version'              => $this->model_extension_module_facebook_business->getPluginVersion()
                    );
                }
            } else {
                $json['error'] = 'OpenCart Store verification failed!';
            }
        } else {
            $json['error'] = 'Invalid parameters provided! Please contact OpenCart support for assistance.';
        }

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($json));
    }

    public function appStoreWebhook() {
        $json = array();

        if (($this->request->server['REQUEST_METHOD'] == 'POST')) {
            $headers = getallheaders();

            if (isset($headers['X-App-Signature'])) {
                $signature = $headers['X-App-Signature'];
            } elseif (isset($headers['x-app-signature'])) {
                $signature = $headers['x-app-signature'];
            } else {
                $signature = '';
            }

            if ($signature == 'f35ca30bfe0b48aa63404483bef6697e94b3f732') {
                $this->load->model('extension/module/facebook_business');

                if (isset($this->request->post['facebook_system_user_access_token'])) {
                    $this->model_extension_module_facebook_business->installFBE($this->request->post);
                } else {
                    $this->model_extension_module_facebook_business->uninstallFBE();
                }

                $json['success'] = true;
            }
        }

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($json));
    }

    private function estimateFeedGenerationTime() {
        $product_total = $this->model_catalog_product->getTotalProducts();

        $num_of_samples = $product_total <= 500 ? $product_total : 500;

        if ($num_of_samples == 0) {
            return 30;
        }

        $feed_dryrun_filename = $this->getWritableProductFeedDir() . 'fbe_feed_dryrun.txt';
        $feed_dryrun_file = fopen($feed_dryrun_filename, 'ab');
        
        $start_time = time();
        $this->writeProductFeedFileInBatch($num_of_samples, $feed_dryrun_file);
        $end_time = time();

        $time_spent = $end_time - $start_time;

        $time_estimate = $time_spent * $product_total / $num_of_samples * 1.5 + 30;

        $time_previous_avg = $this->config->get('facebook_feed_runtime_avg');

        if (!$time_previous_avg) {
            $time_previous_avg = 0;
        }

        return max($time_estimate, $time_previous_avg);
    }
  
    private function estimateFeedGenerationTimeWithDecay($feed_gen_time) {
        // Update feed generation online time estimate w/ 25% decay.
        $old_feed_gen_time = $this->config->get('facebook_feed_runtime_avg');

        if (!$old_feed_gen_time) {
            $old_feed_gen_time = 0;
        }

        if ($feed_gen_time < $old_feed_gen_time) {
            $feed_gen_time = $feed_gen_time * 0.25 + $old_feed_gen_time * 0.75;
        }

        $data = array(
            'facebook_feed_runtime_avg' => $feed_gen_time
        );

        $this->model_extension_module_facebook_business->updateFacebookSettings($data);
    }

    private function sendFileResponse($filename) {
        if (!headers_sent()) {
            header('Content-Type: text/csv; charset=utf-8');
            header('Content-Disposition: attachment; filename="'.basename($filename.'"'));
            header('Expires: 0');
            header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
            header('Pragma: public');
            header('Content-Length:'.filesize($filename));
      
            if (ob_get_level()) {
                ob_end_clean();
            }
      
            readfile($filename, 'rb');
      
            exit();
        }
    }

    private function getProductFeedPath() {
        $product_feed_dir = $this->getWritableProductFeedDir();

        if (!$product_feed_dir) {
            return false;
        }

        return $product_feed_dir . $this->facebook_feed_filename;
    }

    private function getWritableProductFeedDir() {
        // Checks on 3 folders if they are writable and return a folder if so
        if (is_writable(DIR_DOWNLOAD)) {
            return DIR_DOWNLOAD;
        }

        if (is_writable(DIR_MODIFICATION)) {
            return DIR_MODIFICATION;
        }

        if (is_writable(DIR_LOGS)) {
            return DIR_LOGS;
        }

        return false;
    }

    private function isFeedFileStale($filepath) {
        $time_file_modified = file_exists($filepath) ? filemtime($filepath) : 0;

        /**
         * If no file last modified time is available, or the last modified time
         * is 1 hour ago, we count it as 'stale'
         */
        if (!$time_file_modified) {
            return true;
        } else {
            return time() - $time_file_modified > 3600;
        }
    }

    private function generateProductFeedFile($product_feed_path) {
        try {
            $feed_file = fopen($product_feed_path, "ab");

            if (!$this->writeProductFeedFileHeader($feed_file)) {
                fclose($feed_file);
                return false;
            }

            // Process products in batches to handle large product catalogs
            $filter_data = array(
                'filter_status' => 1
            );

            $product_total = $this->model_catalog_product->getTotalProducts($filter_data);
      
            return $this->writeProductFeedFileInBatch($product_total, $feed_file);
        } catch (Exception $e) {
            // Handle any exceptions during the feed file generation
            if (isset($feed_file) && !!($feed_file)) {
                fclose($feed_file);
            }

            return false;
        }
    }

    private function writeProductFeedFileHeader($feed_file) {
        $product_feed_header_row = 'id,title,description,rich_text_description,image_link,link,google_product_category,brand,price,' . 
            'availability,item_group_id,additional_image_link,sale_price,sale_price_effective_date,condition,' . 
            'age_group,color,gender,material,pattern' . PHP_EOL;

        try {
            fputs($feed_file, "\xEF\xBB\xBF");
            fwrite($feed_file, $product_feed_header_row);
            return true;
        } catch (Exception $e) {
            return false;
        }
    }

    private function writeProductFeedFileInBatch($product_total, $feed_file) {
        $total_batches = ceil($product_total / 100);

        for ($batch_number = 0; $batch_number < $total_batches; $batch_number++) {
            $filter_data = array(
                'start' => $batch_number * 100,
                'limit' => 100,
                'filter_status' => 1
            );

            $products = $this->model_extension_module_facebook_business->getProducts($filter_data);

            if (isset($products) && sizeof($products) > 0) {
                if (!$this->writeProductFeedFile($products, $feed_file)) {
                    fclose($feed_file);
                    return false;
                }
            }
        }

        // Feed file generation is successful
        fclose($feed_file);
        return true;
    }

    private function writeProductFeedFile($products, $feed_file) {
        $product_data = array();

        try {
            foreach ($products as $product) {
                $formatted_product_data = $this->formatProductDetails($product);

                if ($formatted_product_data) {
                    fwrite($feed_file, $this->convertProductDataAsFeedRow($formatted_product_data));
                }
            }

            return true;
        } catch (Exception $e) {
            return false;
        }
    }

    private function convertProductDataAsFeedRow($product_data) {
        $row = '';

        $count = count($product_data);

        foreach ($product_data as $product) {
            $count--;

            if ($count == 0) {
                $row .= $product . PHP_EOL;
            } else {
                $row .= $product . ',';
            }
        }

        return $row;
    }

    private function formatAndTrimString($text, $length = false) {
        if ($text) {
            $text = trim(strip_tags(html_entity_decode(html_entity_decode($text), ENT_QUOTES | ENT_COMPAT, 'UTF-8')));

            if ($length && strlen($text) > $length) {
                $text = substr($text, 0, $length);
            }

            $text = '"' . str_replace('"', '""', $text) . '"';

            return $text;
        } else {
            return '""';
        }
    }

    private function getStoreBaseUrl() {
        if ($this->config->get('config_ssl')) {
            return HTTP_SERVER;
        } else {
            return HTTPS_SERVER;
        }
    }

    private function formatProductDetails($product_info) {
        $special_info = $this->model_extension_module_facebook_business->getProductSpecials($product_info['product_id']);

        if ($special_info) {
            $product_info = array_merge($product_info, $special_info);
        }

        $formatted_product_details = array(
            'retailer_id'                 => $product_info['product_id'],
            'name'                        => $this->getName($product_info),
            'description'                 => $this->getDescription($product_info),
            'rich_text_description'       => $this->getRichTextDescription($product_info),
            'image_url'                   => $this->formatAndTrimString($this->getImageUrl($product_info['image'])),
            'product_url'                 => $this->getProductUrl($product_info['product_id']),
            'category'                    => $this->getCategory($product_info),
            'brand'                       => $this->getBrand($product_info),
            'price'                       => $this->getPrice($product_info),
            'availability'                => $this->getAvailability($product_info),
            'retailer_product_group_id'   => $product_info['product_id'],
            'additional_image_urls'       => $this->getAdditionalImageUrls($product_info['product_id']),
            'special'                     => $this->getSpecialPrice($product_info),
            'special_period'              => $this->getSpecialPricePeriod($product_info),
            'condition'                   => $this->getCondition($product_info['product_id'])
        );

        $additional_details = $this->getAdditionalDetails($product_info['product_id']);
        $formatted_product_details = array_merge($formatted_product_details, $additional_details);

        // Ensure that the product checks all requirements for the product feed
        if (in_array(false, $formatted_product_details, true) === true) {
            return false;
        } else {
            return $formatted_product_details;
        }
    }

    private function getName($product_info) {
        return $this->formatAndTrimString($product_info['name'], 150);
    }

    private function getDescription($product_info) {
        $description = $this->formatAndTrimString($product_info['description'], 5000);

        // Fallback to Meta Description if description is not available
        if (!$description) {
            $description = $this->formatAndTrimString($product_info['meta_description'], 5000);
        }

        // Fallback to Product Name if Description and Meta Description are not available
        if (!$description) {
            $description = $this->formatAndTrimString($product_info['name'], 5000);
        }

        // If description doesn't contain non-English characters, check if all Uppercase
        if (strlen($description) == strlen(utf8_decode($description))) {
            if (strtoupper($description) == $description) {
                $description = ucfirst(strtolower($description));
            }
        }

        return $description;
    }

    private function getRichTextDescription($product_info) {
        $description = html_entity_decode($product_info['description'], ENT_QUOTES, 'UTF-8');

        $description = '"' . str_replace('"', '""', $description) . '"';

        return $description;
    }

    private function getImageUrl($image) {
        // Cater for cases where the image is an external URL
        if (filter_var($image, FILTER_VALIDATE_URL)) {
            return $image;
        } else {
            return $this->getStoreBaseUrl() . 'image/' . $image;
        }
    }

    private function getAdditionalImageUrls($product_id) {
        $formatted_images = '';

        $product_images = $this->model_catalog_product->getProductImages($product_id);

        // Limit of up to 20 images
        $product_images = array_slice($product_images, 0, 20);

        foreach ($product_images as $product_image) {
            $image_url = $this->getImageUrl($product_image['image']);

            // Limit of up to 2000 characters
            if (strlen($formatted_images . $image_url) < 2000) {
                if ($formatted_images) {
                    $formatted_images .= ',' . $image_url;
                } else {
                    $formatted_images .= $image_url;
                }
            } else {
                break;
            }
        }

        return $this->formatAndTrimString($formatted_images);
    }

    private function getProductUrl($product_id) {
        $product_url = $this->url->link('product/product', 'product_id=' . (int)$product_id, true);

        return $this->formatAndTrimString($product_url);
    }

    private function getCategory($product_info) {
        $product_to_facebook = $this->model_extension_module_facebook_business->getProductToFacebook($product_info['product_id']);

        if ($product_to_facebook['google_product_category']) {
            return $product_to_facebook['google_product_category'];
        } else {
            $category = $product_info['category_name'];

            if (!$category) {
                $category = $this->config->get('config_name');
            }
    
            return $this->formatAndTrimString($category);
        }
    }

    private function getBrand($product_info) {
        if ($product_info['manufacturer_name']) {
            $brand = $product_info['manufacturer_name'];
        } else {
            $brand = $this->config->get('config_name');
        }

        return $this->formatAndTrimString($brand);
    }

    private function getPrice($product_info) {
        if ($product_info['tax_class_id']) {
            $price = $this->tax->calculate($product_info['price'], $product_info['tax_class_id'], $this->config->get('config_tax'));
        } else {
            $price = $product_info['price'];
        }

        $price = number_format(round((float)$price, 2), 2, '.', '');
        $price = $price . ' ' . strtoupper($this->config->get('config_currency'));

        return $price;
    }

    private function getSpecialPrice($product_info) {
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



3.0.x-and-above/upload/catalog/controller/extension/module/facebook_business.php [40:557]:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                        return false;
                    }
                }
            } else {
                return false;
            }

            $end_time = time();
            $feed_gen_time = $end_time - $start_time;
        
            $this->estimateFeedGenerationTimeWithDecay($feed_gen_time);
      
            // genFeedPing return time estimation only
            if (isset($this->request->get['from']) && $this->request->get['from'] == 'genFeedPing') {
                return;
            }
      
            $this->sendFileResponse($product_feed_path);
        } catch (Exception $e) {
            $this->response->addHeader('Content-type: text');
            $this->response->setOutput('There was a problem generating your feed: %s', $e->getMessage());
        }
    }

    public function genFeedNow() {
        $this->genFeed(true);
    }

    public function genFeedPing() {
        $this->load->model('catalog/product');
        $this->load->model('extension/module/facebook_business');

        $time = $this->estimateFeedGenerationTime();
        $this->response->addHeader('Content-type: text/plain');
        $this->response->setOutput(round($time));

        // This will call the genAction method above in an async request
        // so that we can still return a response from the ping action.
        try {
            $url = HTTP_SERVER.'index.php?route=extension/module/facebook_business/genFeed&from=genFeedPing';
            $curl = curl_init($url);
            curl_setopt($curl, CURLOPT_TIMEOUT, 1);
            curl_exec($curl);
            curl_close($curl);
        } catch (Exception $e) {
            // We expect the result to time out.
        }
    }

    public function appStorePing() {
        $json = array();

        $this->load->model('extension/module/facebook_business');
        $json['version'] = $this->model_extension_module_facebook_business->getPluginVersion();
        $json['store_url'] = $this->url->link('extension/module/facebook_business', '', true);

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($json));
    }

    public function appStoreConnect() {
        $json = array();

        $this->load->model('extension/module/facebook_business');

        if (($this->request->server['REQUEST_METHOD'] == 'POST') && isset($this->request->post['store_url']) && isset($this->request->post['username']) && isset($this->request->post['password'])) {
            if (strpos(HTTPS_SERVER, $this->request->post['store_url']) !== false && $this->model_extension_module_facebook_business->isVerifiedAdminUser($this->request->post['username'], $this->request->post['password'])) {
                if ($this->model_extension_module_facebook_business->isExtensionInstalled()) {
                    if ($this->config->get('facebook_fbe_v2_installed')) {
                        $json['error'] = 'Your store is already connected to a Facebook Business Page.';
                    } elseif (!$this->model_extension_module_facebook_business->isVerifiedAdminUser($this->request->post['username'], $this->request->post['password'], 'extension/module/facebook_business')) {
                        $json['error'] = 'OpenCart Admin user does not have the correct permissions to perform this action!';
                    }
                } elseif (!$this->model_extension_module_facebook_business->isVerifiedAdminUser($this->request->post['username'], $this->request->post['password'], 'extension/extension/module', true)) {
                    $json['error'] = 'OpenCart Admin user does not have the correct permissions to perform this action!';
                }

                if (!$json) {
                    $json['success'] = true;
                    $json['fbe_params'] = array(
                        'external_business_id' => HTTPS_SERVER,
                        'business_name'        => $this->config->get('config_name') ? $this->config->get('config_name') : 'My Business',
                        'feed_url'             => $this->url->link('extension/module/facebook_business/genFeed', '', true),
                        'feed_ping_url'        => $this->url->link('extension/module/facebook_business/genFeedPing', '', true),
                        'timezone'             => date_default_timezone_get(),
                        'currency'             => strtoupper(addslashes($this->config->get('config_currency'))),
                        'version'              => $this->model_extension_module_facebook_business->getPluginVersion()
                    );
                }
            } else {
                $json['error'] = 'OpenCart Store verification failed!';
            }
        } else {
            $json['error'] = 'Invalid parameters provided! Please contact OpenCart support for assistance.';
        }

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($json));
    }

    public function appStoreWebhook() {
        $json = array();

        if (($this->request->server['REQUEST_METHOD'] == 'POST')) {
            $headers = getallheaders();

            if (isset($headers['X-App-Signature'])) {
                $signature = $headers['X-App-Signature'];
            } elseif (isset($headers['x-app-signature'])) {
                $signature = $headers['x-app-signature'];
            } else {
                $signature = '';
            }

            if ($signature == 'f35ca30bfe0b48aa63404483bef6697e94b3f732') {
                $this->load->model('extension/module/facebook_business');

                if (isset($this->request->post['facebook_system_user_access_token'])) {
                    $this->model_extension_module_facebook_business->installFBE($this->request->post);
                } else {
                    $this->model_extension_module_facebook_business->uninstallFBE();
                }

                $json['success'] = true;
            }
        }

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($json));
    }

    private function estimateFeedGenerationTime() {
        $product_total = $this->model_catalog_product->getTotalProducts();

        $num_of_samples = $product_total <= 500 ? $product_total : 500;

        if ($num_of_samples == 0) {
            return 30;
        }

        $feed_dryrun_filename = $this->getWritableProductFeedDir() . 'fbe_feed_dryrun.txt';
        $feed_dryrun_file = fopen($feed_dryrun_filename, 'ab');
        
        $start_time = time();
        $this->writeProductFeedFileInBatch($num_of_samples, $feed_dryrun_file);
        $end_time = time();

        $time_spent = $end_time - $start_time;

        $time_estimate = $time_spent * $product_total / $num_of_samples * 1.5 + 30;

        $time_previous_avg = $this->config->get('facebook_feed_runtime_avg');

        if (!$time_previous_avg) {
            $time_previous_avg = 0;
        }

        return max($time_estimate, $time_previous_avg);
    }
  

    private function estimateFeedGenerationTimeWithDecay($feed_gen_time) {
        // Update feed generation online time estimate w/ 25% decay.
        $old_feed_gen_time = $this->config->get('facebook_feed_runtime_avg');

        if (!$old_feed_gen_time) {
            $old_feed_gen_time = 0;
        }

        if ($feed_gen_time < $old_feed_gen_time) {
            $feed_gen_time = $feed_gen_time * 0.25 + $old_feed_gen_time * 0.75;
        }

        $data = array(
            'facebook_feed_runtime_avg' => $feed_gen_time
        );

        $this->model_extension_module_facebook_business->updateFacebookSettings($data);
    }

    private function sendFileResponse($filename) {
        if (!headers_sent()) {
            header('Content-Type: text/csv; charset=utf-8');
            header('Content-Disposition: attachment; filename="'.basename($filename.'"'));
            header('Expires: 0');
            header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
            header('Pragma: public');
            header('Content-Length:'.filesize($filename));
      
            if (ob_get_level()) {
                ob_end_clean();
            }
      
            readfile($filename, 'rb');
      
            exit();
        }
    }

    private function getProductFeedPath() {
        $product_feed_dir = $this->getWritableProductFeedDir();

        if (!$product_feed_dir) {
            return false;
        }

        return $product_feed_dir . $this->facebook_feed_filename;
    }

    private function getWritableProductFeedDir() {
        // Checks on 3 folders if they are writable and return a folder if so
        if (is_writable(DIR_DOWNLOAD)) {
            return DIR_DOWNLOAD;
        }

        if (is_writable(DIR_MODIFICATION)) {
            return DIR_MODIFICATION;
        }

        if (is_writable(DIR_LOGS)) {
            return DIR_LOGS;
        }

        return false;
    }

    private function isFeedFileStale($filepath) {
        $time_file_modified = file_exists($filepath) ? filemtime($filepath) : 0;

        /**
         * If no file last modified time is available, or the last modified time
         * is 1 hour ago, we count it as 'stale'
         */
        if (!$time_file_modified) {
            return true;
        } else {
            return time() - $time_file_modified > 3600;
        }
    }

    private function generateProductFeedFile($product_feed_path) {
        try {
            $feed_file = fopen($product_feed_path, "ab");
      
            if (!$this->writeProductFeedFileHeader($feed_file)) {
                fclose($feed_file);
                return false;
            }

            // Process products in batches to handle large product catalogs
            $filter_data = array(
                'filter_status' => 1
            );

            $product_total = $this->model_catalog_product->getTotalProducts($filter_data);
      
            return $this->writeProductFeedFileInBatch($product_total, $feed_file);
        } catch (Exception $e) {
            // Handle any exceptions during the feed file generation
            if (isset($feed_file) && !!($feed_file)) {
                fclose($feed_file);
            }

            return false;
        }
    }

    private function writeProductFeedFileHeader($feed_file) {
        $product_feed_header_row = 'id,title,description,rich_text_description,image_link,link,google_product_category,brand,price,' . 
            'availability,item_group_id,additional_image_link,sale_price,sale_price_effective_date,condition,' . 
            'age_group,color,gender,material,pattern' . PHP_EOL;

        try {
            fputs($feed_file, "\xEF\xBB\xBF");
            fwrite($feed_file, $product_feed_header_row);
            return true;
        } catch (Exception $e) {
            return false;
        }
    }

    private function writeProductFeedFileInBatch($product_total, $feed_file) {
        $total_batches = ceil($product_total / 100);

        for ($batch_number = 0; $batch_number < $total_batches; $batch_number++) {
            $filter_data = array(
                'start' => $batch_number * 100,
                'limit' => 100,
                'filter_status' => 1
            );

            $products = $this->model_extension_module_facebook_business->getProducts($filter_data);

            if (isset($products) && sizeof($products) > 0) {
                if (!$this->writeProductFeedFile($products, $feed_file)) {
                    fclose($feed_file);
                    return false;
                }
            }
        }

        // Feed file generation is successful
        fclose($feed_file);
        return true;
    }

    private function writeProductFeedFile($products, $feed_file) {
        $product_data = array();

        try {
            foreach ($products as $product) {
                $formatted_product_data = $this->formatProductDetails($product);

                if ($formatted_product_data) {
                    fwrite($feed_file, $this->convertProductDataAsFeedRow($formatted_product_data));
                }
            }

            return true;
        } catch (Exception $e) {
            return false;
        }
    }

    private function convertProductDataAsFeedRow($product_data) {
        $row = '';

        $count = count($product_data);

        foreach ($product_data as $product) {
            $count--;

            if ($count == 0) {
                $row .= $product . PHP_EOL;
            } else {
                $row .= $product . ',';
            }
        }

        return $row;
    }

    private function formatAndTrimString($text, $length = false) {
        if ($text) {
            $text = trim(strip_tags(html_entity_decode(html_entity_decode($text), ENT_QUOTES | ENT_COMPAT, 'UTF-8')));

            if ($length && strlen($text) > $length) {
                $text = substr($text, 0, $length);
            }

            $text = '"' . str_replace('"', '""', $text) . '"';

            return $text;
        } else {
            return '""';
        }
    }

    private function getStoreBaseUrl() {
        if ($this->config->get('config_ssl')) {
            return HTTP_SERVER;
        } else {
            return HTTPS_SERVER;
        }
    }

    private function formatProductDetails($product_info) {
        $special_info = $this->model_extension_module_facebook_business->getProductSpecials($product_info['product_id']);

        if ($special_info) {
            $product_info = array_merge($product_info, $special_info);
        }

        $formatted_product_details = array(
            'retailer_id'                 => $product_info['product_id'],
            'name'                        => $this->getName($product_info),
            'description'                 => $this->getDescription($product_info),
            'rich_text_description'       => $this->getRichTextDescription($product_info),
            'image_url'                   => $this->formatAndTrimString($this->getImageUrl($product_info['image'])),
            'product_url'                 => $this->getProductUrl($product_info['product_id']),
            'category'                    => $this->getCategory($product_info),
            'brand'                       => $this->getBrand($product_info),
            'price'                       => $this->getPrice($product_info),
            'availability'                => $this->getAvailability($product_info),
            'retailer_product_group_id'   => $product_info['product_id'],
            'additional_image_urls'       => $this->getAdditionalImageUrls($product_info['product_id']),
            'special'                     => $this->getSpecialPrice($product_info),
            'special_period'              => $this->getSpecialPricePeriod($product_info),
            'condition'                   => $this->getCondition($product_info['product_id'])
        );

        $additional_details = $this->getAdditionalDetails($product_info['product_id']);
        $formatted_product_details = array_merge($formatted_product_details, $additional_details);

        // Ensure that the product checks all requirements for the product feed
        if (in_array(false, $formatted_product_details, true) === true) {
            return false;
        } else {
            return $formatted_product_details;
        }
    }

    private function getName($product_info) {
        return $this->formatAndTrimString($product_info['name'], 150);
    }

    private function getDescription($product_info) {
        $description = $this->formatAndTrimString($product_info['description'], 5000);

        // Fallback to Meta Description if description is not available
        if (!$description) {
            $description = $this->formatAndTrimString($product_info['meta_description'], 5000);
        }

        // Fallback to Product Name if Description and Meta Description are not available
        if (!$description) {
            $description = $this->formatAndTrimString($product_info['name'], 5000);
        }

        // If description doesn't contain non-English characters, check if all Uppercase
        if (strlen($description) == strlen(utf8_decode($description))) {
            if (strtoupper($description) == $description) {
                $description = ucfirst(strtolower($description));
            }
        }

        return $description;
    }

    private function getRichTextDescription($product_info) {
        $description = html_entity_decode($product_info['description'], ENT_QUOTES, 'UTF-8');

        $description = '"' . str_replace('"', '""', $description) . '"';

        return $description;
    }

    private function getImageUrl($image) {
        // Cater for cases where the image is an external URL
        if (filter_var($image, FILTER_VALIDATE_URL)) {
            return $image;
        } else {
            return $this->getStoreBaseUrl() . 'image/' . $image;
        }
    }

    private function getAdditionalImageUrls($product_id) {
        $formatted_images = '';

        $product_images = $this->model_catalog_product->getProductImages($product_id);

        // Limit of up to 20 images
        $product_images = array_slice($product_images, 0, 20);

        foreach ($product_images as $product_image) {
            $image_url = $this->getImageUrl($product_image['image']);

            // Limit of up to 2000 characters
            if (strlen($formatted_images . $image_url) < 2000) {
                if ($formatted_images) {
                    $formatted_images .= ',' . $image_url;
                } else {
                    $formatted_images .= $image_url;
                }
            } else {
                break;
            }
        }

        return $this->formatAndTrimString($formatted_images);
    }

    private function getProductUrl($product_id) {
        $product_url = $this->url->link('product/product', 'product_id=' . (int)$product_id, true);

        return $this->formatAndTrimString($product_url);
    }

    private function getCategory($product_info) {
        $product_to_facebook = $this->model_extension_module_facebook_business->getProductToFacebook($product_info['product_id']);

        if ($product_to_facebook['google_product_category']) {
            return $product_to_facebook['google_product_category'];
        } else {
            $category = $product_info['category_name'];

            if (!$category) {
                $category = $this->config->get('config_name');
            }
    
            return $this->formatAndTrimString($category);
        }
    }

    private function getBrand($product_info) {
        if ($product_info['manufacturer_name']) {
            $brand = $product_info['manufacturer_name'];
        } else {
            $brand = $this->config->get('config_name');
        }

        return $this->formatAndTrimString($brand);
    }

    private function getPrice($product_info) {
        if ($product_info['tax_class_id']) {
            $price = $this->tax->calculate($product_info['price'], $product_info['tax_class_id'], $this->config->get('config_tax'));
        } else {
            $price = $product_info['price'];
        }

        $price = number_format(round((float)$price, 2), 2, '.', '');
        $price = $price . ' ' . strtoupper($this->config->get('config_currency'));

        return $price;
    }

    private function getSpecialPrice($product_info) {
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



