<?php
/**
 * @package		SeoPro
 * @author		Oclabs
 * @copyright	Copyright (c) 2017, Oclabs (https://www.oclabs.pro/)
 * @copyright	Copyright (c) 2021, ocStore (https://ocstore.com/)
 * @license		https://opensource.org/licenses/GPL-3.0
 */

// ALTER TABLE `oc_product_to_category` ADD `main_category_id` TINYINT(1) NOT NULL DEFAULT '0' AFTER `category_id`;

class SeoPro {
	private $config;
	private $ajax = false;
	private $request;
	private $registry;
	private $response;
	private $url;
	private $session;
	private $db;
	private $cache;
	private $cat_tree = [];
	private $keywords = [];
	private $queries = [];
	private $product_categories = [];
	private $valide_get_param;

	public function __construct($registry) {
		$this->detectAjax();
		$this->registry = $registry;
		$this->config = $registry->get('config');

		if(!$this->config->get('config_seo_pro')) {
			return;
		}

		$this->request = $registry->get('request');
		$this->session = $registry->get('session');
		$this->response = $registry->get('response');
		$this->url = $registry->get('url');
		$this->db = $registry->get('db');
		$this->cache = $registry->get('cache');
		$this->detectPostfix();
		$this->detectLanguage();
		$this->initHelpers();
		
		$params = explode ("\r\n", $this->config->get('config_valide_params'));
		if(!empty($params)) {
			$this->valide_get_param = $params;
		}
	}

	public function prepareRoute($parts) {
		if (!empty($parts) && is_array($parts)) {
			foreach($parts as $id => $part) {
				if($this->config->get('config_seopro_lowercase')) {
					$parts[$id] = utf8_strtolower($part);
				}

				if($parts[$id]) {
					$query = $this->getQueryByKeyword($parts[$id]);

					$url = explode('=', ($query ? $query : ''));

					if(!empty($url[0])) {
						if(!in_array($url[0], ['category_id', 'product_id', 'manufacturer_id', 'information_id', 'article_id', 'blog_category_id'])) {
							return $parts;
						}

						if ($url[0] == 'category_id') {
							if (!isset($this->request->get['path'])) {
								$this->request->get['path'] = $url[1];
							} else {
								$this->request->get['path'] .= '_' . $url[1];
							}
						} elseif ($url[0] == 'blog_category_id') {
							if (!isset($this->request->get['blog_category_id'])) {
								$this->request->get['blog_category_id'] = $url[1];
							} else {
								$this->request->get['blog_category_id'] .= '_' . $url[1];
							}
						} elseif (count($url) > 1) {
							$this->request->get[$url[0]] = $url[1];
						}
					}
				}

				unset($parts[$id]);
			}

			if(!$query) {
				$this->request->get['route'] = 'error/not_found';
				
				return [];
			}
		}

		if (isset($this->request->get['product_id'])) {
			if(isset($this->request->get['path'])) {
				unset($this->request->get['path']);
			}
			
			$path = $this->getCategoryByProduct($this->request->get['product_id']);
			
			if ($path) {
				$this->request->get['path'] = $path;
			}
			
			$this->request->get['route'] = 'product/product';
		} elseif (isset($this->request->get['path'])) {
			$this->request->get['route'] = 'product/category';
		} elseif (isset($this->request->get['manufacturer_id'])) {
			$this->request->get['route'] = 'product/manufacturer/info';
		} elseif (isset($this->request->get['information_id'])) {
			$this->request->get['route'] = 'information/information';
		}

		if (isset($this->request->get['article_id'])) {
			if(isset($this->request->get['blog_category_id'])) {
				unset($this->request->get['blog_category_id']);
			}
			
			$blog_category_path = $this->getBlogPathByArticle($this->request->get['article_id']);
			
			if ($blog_category_path) {
				$this->request->get['blog_category_id'] = $blog_category_path;
			}
			
			$this->request->get['route'] = 'blog/article';
		} elseif (isset($this->request->get['blog_category_id'])) {
			$this->request->get['route'] = 'blog/category';
		}

		return $parts;
	}

	public function baseRewrite($data, $language_id) {

		$url = null;
		$postfix = null;
		$language_id = (int)$this->config->get('config_language_id');

		switch ($data['route']) {
			case 'product/product':
				if (isset($data['product_id'])) {
					$route = 'product/product';
					$path = '';
					$product_id = $data['product_id'];
					
					if (isset($data['path']) || $this->config->get('config_seo_url_include_path')) {
						$path = $this->getCategoryByProduct($product_id);
					}

					if ($this->valide_get_param) {
						$valide_get_param_data = [];
						
						foreach($this->valide_get_param as $get_param) {
							if (isset($data[$get_param])) {
								$valide_get_param_data[$get_param] = $data[$get_param];
								$this->response->addHeader('X-Robots-Tag: noindex');
							}
						}
					}

					unset($data);
					
					$data['route'] = $route;

					if ($path && $this->config->get('config_seo_url_include_path')) {
						$data['path'] = $path;
					}

					$data['product_id'] = $product_id;

					if ($this->valide_get_param) {
						$data = array_merge($data, $valide_get_param_data);
					}
				}
				
				break;
			case 'blog/article':
				if (isset($data['article_id'])) {
					$route = 'blog/article';
					$blog_path = '';
					$article_id = $data['article_id'];

					if (isset($data['blog_category_id']) || $this->config->get('config_seo_url_include_path')) {
						$blog_path = $this->getBlogPathByArticle($article_id);
					}

					if ($this->valide_get_param) {
						$valide_get_param_data = [];
						
						foreach($this->valide_get_param as $get_param) {
							if (isset($data[$get_param])) {
								$valide_get_param_data[$get_param] = $data[$get_param];

								$this->response->addHeader('X-Robots-Tag: noindex');
							}
						}
					}

					unset($data);
					
					$data['route'] = $route;

					if ($blog_path && $this->config->get('config_seo_url_include_path')) {
						$data['blog_category_id'] = $blog_path;
					}

					$data['article_id'] = $article_id;

					if ($this->valide_get_param) {
						$data = array_merge($data, $valide_get_param_data);
					}
				}
				
				break;
			case 'product/category':
				if (isset($data['path'])) {
					$category = explode('_', $data['path']);
					$category = end($category);
					
					unset($data['information_id']);
					
					$data['path'] = $this->getPathByCategory($category);
				}
				
				break;
			case 'product/product/review':
			case 'blog/article/review':
			case 'information/information/agree':
				return [$url, $data, $postfix];
				
				break;
			case 'information/information/info':
			case 'product/manufacturer/info':
			
				break;
			default:
				break;
		}

		$queries = [];

		$route = '';
		
		if (isset($data['route'])) {
			$route = $data['route'];
			unset($data['route']);
		}

		foreach ($data as $key => $value) {
			switch ($key) {
				case 'product_id':
					$product_id = (int)$value;
					$queries[] = 'product_id=' . $product_id;
					$postfix = true;
					unset($data[$key]);
					break;
				case 'manufacturer_id':
					$manufacturer_id = (int)$value;
					$queries[] = 'manufacturer_id=' . $manufacturer_id;
					$postfix = true;
					unset($data[$key]);
					break;
				case 'category_id':
				case 'information_id':
					$information_id = (int)$value;
					$queries[] = 'information_id=' . $information_id;
					$postfix = true;
					unset($data[$key]);
					break;
				case 'blog_category_id':
					$blog_categories = explode('_', $value);
					
					foreach ($blog_categories as $blog_category_id) {
						$queries[] = 'blog_category_id=' . (int)$blog_category_id;
					}				
					unset($data[$key]);			
					break;
				case 'article_id':
					$article_id = (int)$value;
					$queries[] = 'article_id=' . $article_id;
					$postfix = true;
					unset($data[$key]);			
					break;
				case 'path':
					$categories = explode('_', $value);				
					foreach ($categories as $category_id) {
						$queries[] = 'category_id=' . (int)$category_id;
					}					
					unset($data[$key]);					
					break;
				default:
					break;
			}
		}

		if (empty($queries) && $route) {
			$keyword = $this->getKeywordByQuery($route);

			if($keyword !== null) {
				$url = '';
				
				if($keyword !== '') {
					$url = '/' . rawurlencode($keyword);
				}
			}

			$data['route'] = $route;
		} else {
			$rows = [];

			foreach ($queries as $query) {
				$keyword = $this->getKeywordByQuery($query);
				
				if ($keyword) {
					$rows[] = $keyword;
				}
			}

			if (!empty($rows) && (count($rows) == count($queries))) {
				foreach($rows as $row) {
					$url .= '/' . rawurlencode($row);
				}
			}
		}

		return [$url, $data, $postfix];
	}

	private function getPath($categories, $category_id, $current_path = []) {
		if(!$current_path) {
			$current_path = [(int)$category_id];
		}

		$path = $current_path;

		$parent_id = 0;

		if(isset($categories[$category_id]['parent_id'])) {
			$parent_id = (int)$categories[$category_id]['parent_id'];
		}

		if($parent_id > 0) {
			$new_path = array_merge([$parent_id], $current_path);
			$path = $this->getPath($categories, $parent_id, $new_path);
		}

		return $path;
	}

	private function initHelpers() {
		if($this->config->get('config_seo_url_cache')){
			$this->cat_tree = $this->cache->get('seopro.cat_tree');
		}

		if(!$this->cat_tree || empty($this->cat_tree)) {
			$this->cat_tree = [];

			$all_cat_query = $this->db->query("SELECT category_id, parent_id FROM " . DB_PREFIX . "category ORDER BY parent_id");

			$allcats = [];
			$categories = [];

			if($all_cat_query->num_rows) {
				$allcats = $all_cat_query->rows;
			}

			foreach ($allcats as $category) {
				$categories[$category['category_id']]['parent_id'] = $category['parent_id'];
			}
			
			unset ($allcats);

			foreach ($categories as $category_id => $category) {
				$path = $this->getPath($categories, $category_id);
				$this->cat_tree[$category_id]['path'] = $path;
			}
		}

		if ($this->config->get('config_seo_url_cache')) {
			$this->keywords = $this->cache->get('seopro.keywords');
			$this->queries = $this->cache->get('seopro.queries');

			if (!$this->keywords || !is_array($this->keywords)) {
				$this->keywords = [];
			}
		
			if (!$this->queries || !is_array($this->queries)) {
				$this->queries = [];
			}

			if (empty($this->keywords) || empty($this->queries)) {
				$sql_keyword = 'keyword';
				
				if ($this->config->get('config_seopro_lowercase')) {
					$sql_keyword = 'LCASE(keyword) as ' . $sql_keyword;
				}

				$sql = "SELECT " . $sql_keyword . ", query, store_id, language_id FROM " . DB_PREFIX . "seo_url WHERE 1";
				$query = $this->db->query($sql);
				
				if ($query->num_rows && is_array($query->rows)) {
					foreach ($query->rows as $row) {
						$this->keywords[$row['query']][$row['store_id']][$row['language_id']] = $row['keyword'];
						$this->queries[$row['keyword']][$row['store_id']][$row['language_id']] = $row['query'];
					}
				}
			}
		}
	}

	private function detectPostfix() {
		if($this->config->get('config_page_postfix') && isset($this->request->get['_route_'])) {
			$this->request->get['_route_'] = preg_replace('/' . $this->config->get('config_page_postfix') . '$/', '', (string)$this->request->get['_route_']);
		}
	}

	private function addpostfix($url) {
		if($this->config->get('config_page_postfix')) {
			$url = rtrim($url, "/") . $this->config->get('config_page_postfix');
		}
		
		return $url;
	}

	private function getQueryByKeyword($keyword, $language_id = null) {
		$query = null;
		$store_id = (int)$this->config->get('config_store_id');

		if (!$language_id) {
			$language_id = (int)$this->config->get('config_language_id');
		}

		if ($this->config->get('config_seo_url_cache')){
			if (isset($this->queries[$keyword][$store_id][$language_id])) {
				$query = $this->queries[$keyword][$store_id][$language_id];
			}
		} else {
			$_query = $this->db->query("SELECT query FROM " . DB_PREFIX . "seo_url WHERE keyword = '" . $this->db->escape(trim($keyword)) . "' AND store_id = '" . $store_id . "' AND language_id = '" . $language_id . "' LIMIT 1");
			$query = !empty($_query->row) ? (string)$_query->row['query'] : null;
		}

		return $query;
	}

	private function getKeywordByQuery($query, $language_id = null) {
		$keyword = null;
		$store_id = (int)$this->config->get('config_store_id');

		if (!$language_id) {
			$language_id = $this->config->get('config_language_id');
		}

		if ($this->config->get('config_seo_url_cache')) {
			if (isset($this->keywords[$query][$store_id][$language_id])) {
				$keyword = $this->keywords[$query][$store_id][$language_id];
			}
		} else {
			$sql_keyword = 'keyword';

			if ($this->config->get('config_seopro_lowercase')) {
				$sql_keyword = 'LCASE(keyword) as '. $sql_keyword;
			}

			$query = $this->db->query("SELECT " . $sql_keyword . " FROM " . DB_PREFIX . "seo_url WHERE query = '" . $this->db->escape($query) . "' AND store_id = '" . $store_id . "' AND language_id = '" . (int)$language_id . "' LIMIT 1");
			$keyword = !empty($query->row) ? (string)$query->row['keyword'] : null;
		}

		return $keyword;
	}

	public function validate() {
		if (php_sapi_name() === 'cli') {
			return;
		}
		
		if (isset($this->request->get['route'])) {
			$break_routes = [
				'error/not_found',
				'extension/feed/google_sitemap',
				'extension/feed/google_base',
				'extension/feed/sitemap_pro',
				'extension/feed/yandex_feed'
			];

			if (in_array($this->request->get['route'], $break_routes)) {
				return;
			}
		}

		if (!empty($this->request->post)) {
			return;
		}

		if ($this->ajax) {
			$this->response->addHeader('X-Robots-Tag: noindex');
	  
			return;
		}

		if (empty($this->request->get['route'])) {
			$this->request->get['route'] = 'common/home';
		}

		$uri = $this->request->server['REQUEST_URI'];
		$route = $this->request->get['route'];

		if (isset($this->request->get['page'])) {
			if((float)$this->request->get['page'] < 1) {
				unset($this->request->get['page']);
			}
		}

		if ($_SERVER['HTTPS'] == true) {
			$host = substr($this->config->get('config_ssl'), 0, $this->strpos_offset('/', $this->config->get('config_ssl'), 3) + 1);
		} else {
			$host = substr($this->config->get('config_url'), 0, $this->strpos_offset('/', $this->config->get('config_url'), 3) + 1);
		}

		if (!$this->config->get('config_seopro_addslash')) {
			if ($uri == '/') {
				$host = rtrim($host, '/');
			}
		}

		$url = str_replace('&amp;', '&', $host . ltrim($uri, '/'));
		$seo = str_replace('&amp;', '&', $this->url->link($route, $this->getQueryString(array('_route_', 'route')), $_SERVER['HTTPS']));

		if (rawurldecode($url) != rawurldecode($seo)) {
			$this->response->redirect($seo, 301);
		}
	}

	private function detectAjax () {
		if (isset($this->request->server['HTTP_X_REQUESTED_WITH']) && strtolower($this->request->server['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
			$this->ajax = true;
		}
	}

	private function detectLanguage() {
		if ($this->ajax) {
			return;
		}

		$request_language_id = null;
		$request_language_code = '';
		$active_language_id = $this->config->get('config_language_id');

		if (isset($this->request->get['_route_'])) {
			$parts = $parts = explode('/', $this->request->get['_route_']);
			$keyword = end($parts);
		} else {
			$keyword = '';
		}

		if ($keyword || $this->request->server['REQUEST_URI'] == '/') {
			$query = $this->db->query("SELECT language_id FROM " . DB_PREFIX . "seo_url WHERE keyword = '" . $this->db->escape(trim($keyword)) . "' AND store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1");
	  
			if ($query->row) {
				$request_language_id = (int)$query->row['language_id'];

				$query = $this->db->query("SELECT code FROM " . DB_PREFIX . "language WHERE language_id = '" . (int)$request_language_id . "' AND status = '1' LIMIT 1");

				if ($query->row) {
					$request_language_code = $query->row['code'];
					$this->session->data['language'] = $request_language_code;
				}
			}
		}

		if (isset($this->session->data['language'])) {
			$query = $this->db->query("SELECT language_id FROM " . DB_PREFIX . "language WHERE code = '" . (int)$this->session->data['language'] . "' AND status = '1' LIMIT 1");

			if ($query->num_rows) {
				$active_language_id = (int)$query->row['language_id'];
			}
		}

		if($request_language_id && $request_language_code && $active_language_id != $request_language_id) {
			$language = new Language($request_language_code);
			$language->load($request_language_code);
			
			$this->registry->set('language', $language);
			$this->config->set('config_language_id', $request_language_id);
		}
	}

	private function getCategoryByProduct($product_id) {
		if ((int)$product_id < 1) {
			return;
		}

		if ($this->config->get('config_seo_url_cache')) {
			$this->product_categories = $this->cache->get('seopro.product_categories');
			
			if(isset($this->product_categories[$product_id])) {
				return $this->product_categories[$product_id];
			}
		}

		$query = $this->db->query("SELECT category_id FROM " . DB_PREFIX . "product_to_category WHERE product_id = '" . (int)$product_id . "' ORDER BY main_category DESC LIMIT 1");
		$category_id = $this->getPathByCategory($query->num_rows ? (int)$query->row['category_id'] : 0);

		if ($this->config->get('config_seo_url_cache')) {
			if (!is_array($this->product_categories)) {
				$this->product_categories = [];
			}

			$this->product_categories[$product_id] = $category_id;
		}

		return $category_id;
	}

	private function getPathByCategory($category_id) {
		$path = '';

		if ((int)$category_id < 1 && !isset($this->cat_tree[$category_id])) {
			return;
		}

		if (!empty($this->cat_tree[$category_id]['path']) && is_array($this->cat_tree[$category_id]['path'])) {
			$path = implode('_', $this->cat_tree[$category_id]['path']);
		}

		return $path;
	}

	private function getBlogPathByArticle($article_id) {
		if ($article_id < 1) {
			return;
		}

		$query = $this->db->query("SELECT blog_category_id FROM " . DB_PREFIX . "article_to_blog_category WHERE article_id = '" . (int)$article_id . "' ORDER BY main_blog_category DESC LIMIT 1");
		
		$blog_category_path = $this->getBlogPathByCategory($query->num_rows ? (int)$query->row['blog_category_id'] : 0);

		return $blog_category_path;
	}

	private function getBlogPathByCategory($blog_category_id) {
		$blog_category_id = (int)$blog_category_id;
		
		if ($blog_category_id < 1) {
			return;
		}
		
		static $blog_path = [];
		
        $cache = 'seopro.blog_category.seopath';

        if (!$blog_path) {
			if ($this->config->get('config_seo_url_cache')) {
                $blog_path = $this->cache->get($cache);
			}
			
            if (!is_array($blog_path)) {
                $blog_path = [];
			}
        }

		if (!isset($blog_path[$blog_category_id])) {
			$max_level = 10;
			
			$sql = "SELECT CONCAT_WS('_'";
			
			for ($i = $max_level-1; $i >= 0; --$i) {
				$sql .= ",t$i.blog_category_id";
			}
			
			$sql .= ") AS path FROM " . DB_PREFIX . "blog_category t0";
			
			for ($i = 1; $i < $max_level; ++$i) {
				$sql .= " LEFT JOIN " . DB_PREFIX . "blog_category t$i ON (t$i.blog_category_id = t" . ($i-1) . ".parent_id)";
			}
			
			$sql .= " WHERE t0.blog_category_id = '" . $blog_category_id . "'";
			
			$query = $this->db->query($sql);
			
			$blog_path[$blog_category_id] = $query->num_rows ? $query->row['path'] : false;

			if ($this->config->get('config_seo_url_cache')) {
				$this->cache->set($cache, $blog_path);
			}
		}

		return $blog_path[$blog_category_id];
	}

	private function strpos_offset($needle, $haystack, $occurrence) {
		$arr = explode($needle, $haystack);

		switch($occurrence) {
			case $occurrence == 0:
				return;
			case $occurrence > max(array_keys($arr)):
				return;
			default:
				return strlen(implode($needle, array_slice($arr, 0, $occurrence)));
		}
	}

	private function getQueryString($exclude = []) {
		if (!is_array($exclude)) {
			$exclude = [];
		}

		return urldecode(http_build_query(array_diff_key($this->request->get, array_flip($exclude))));
	}

	public function __destruct() {
		if(!$this->config->get('config_seo_pro')) {
			return;
		}

		if ($this->config->get('config_seo_url_cache')){
			$this->cache->set('seopro.keywords', $this->keywords);
			$this->cache->set('seopro.queries', $this->queries);
			$this->cache->set('seopro.cat_tree', $this->cat_tree);
			$this->cache->set('seopro.product_categories', $this->product_categories);
		}
	}
}