На днях мне поставили задачу запустить в кратчайшие сроки интернет-магазин. Я начал искать подходящее решение среди готовых CMS и мой выбор пал на OpenCart (Версия 1.5.5.1). Данная CMS как и все не идеальна и поэтому я начал ковырять ее и изучать как там все устроено.

Первое что я заметил это куча дублей при включенном ЧПУ. Проблема дублей товаров решается при помощи тега rel='canonical' а вот с категориями дело обстояло намного хуже. Каждая категория была доступна по +100500 URL.
Например категория site.ru/category/podcategory будет доступна по следующим URL
site.ru/category/podcategory/a
site.ru/category/podcategory/ab
site.ru/category/podcategory/abc
и так до бесконечности.

Решается проблема не сложно.
Открываем файл catalog/controller/common/seo_url.php ищем 39 строку $this->request->get['route'] = 'error/not_found'; и после ее вставляем
return $this->forward($this->request->get['route']);

Вот исправленный файл целиком
<?php
class ControllerCommonSeoUrl extends Controller {
	public function index() {
		// Add rewrite to url class
		if ($this->config->get('config_seo_url')) {
			$this->url->addRewrite($this);
		}
		
		// Decode URL
		if (isset($this->request->get['_route_'])) {
			$parts = explode('/', $this->request->get['_route_']);
			foreach ($parts as $part) {
				$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "url_alias WHERE keyword = '" . $this->db->escape($part) . "'");
				
				if ($query->num_rows) {
					$url = explode('=', $query->row['query']);
					
					if ($url[0] == 'product_id') {

						$this->request->get['product_id'] = $url[1];
					}
					
					if ($url[0] == 'category_id') {
						if (!isset($this->request->get['path'])) {
							$this->request->get['path'] = $url[1];
						} else {
							$this->request->get['path'] .= '_' . $url[1];
						}
					}	
					
					if ($url[0] == 'manufacturer_id') {
						$this->request->get['manufacturer_id'] = $url[1];
					}
					
					if ($url[0] == 'information_id') {
						$this->request->get['information_id'] = $url[1];
					}	
				} else {
					$this->request->get['route'] = 'error/not_found';
                    return $this->forward($this->request->get['route']);
				}
			}

            if (isset($this->request->get['product_id'])) {
                $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['route'])) {
				return $this->forward($this->request->get['route']);
			}
		}
	}
	
	public function rewrite($link) {
		$url_info = parse_url(str_replace('&', '&', $link));
	
		$url = ''; 
		
		$data = array();
		
		parse_str($url_info['query'], $data);
		
		foreach ($data as $key => $value) {
			if (isset($data['route'])) {
				if (($data['route'] == 'product/product' && $key == 'product_id') || (($data['route'] == 'product/manufacturer/info' || $data['route'] == 'product/product') && $key == 'manufacturer_id') || ($data['route'] == 'information/information' && $key == 'information_id')) {
					$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "url_alias WHERE `query` = '" . $this->db->escape($key . '=' . (int)$value) . "'");
				
					if ($query->num_rows) {
						$url .= '/' . $query->row['keyword'];
						
						unset($data[$key]);
					}					
				} elseif ($key == 'path') {
					$categories = explode('_', $value);
					
					foreach ($categories as $category) {
						$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "url_alias WHERE `query` = 'category_id=" . (int)$category . "'");
				
						if ($query->num_rows) {
							$url .= '/' . $query->row['keyword'];
						}							
					}
					
					unset($data[$key]);
				}
			}
		}
	
		if ($url) {
			unset($data['route']);
		
			$query = '';
		
			if ($data) {
				foreach ($data as $key => $value) {
					$query .= '&' . $key . '=' . $value;
				}
				
				if ($query) {
					$query = '?' . trim($query, '&');
				}
			}

			return $url_info['scheme'] . '://' . $url_info['host'] . (isset($url_info['port']) ? ':' . $url_info['port'] : '') . str_replace('/index.php', '', $url_info['path']) . $url . $query;
		} else {
			return $link;
		}
	}	
}
?>


Эта проблема возникала из-за того что URI /category/podcategory/abc разбивался на массив и при обходе его циклом, каждый элемент массива проверялся, при помощи запроса к БД, соответствует ли он категории или продукту, если нет то для get['route'] присваивалось значение error/not_found.

В нашем случае при первой итерации цикла get['path'] будет присваиваться ID категории «category», при второй итерации get['path'] будет перезаписываться на ID категории «podcategory» при третей итерации get['route'] присваивалось значение error/not_found потому что продукта abc не существует.

Все выглядит хорошо, но в следующем блоке if-ов get['route'] перезаписывается на product/category. Это происходит из-за того что у нас есть get['path'] соответствующее ID категории, что позволяет зайти под условие и перезаписать get['route'].

Данную проблему я решил тем что, возвращаю return $this->forward($this->request->get['route']); сразу же после того как get['route'] станет равным error/not_found не дожидаясь пока она перезапишется.

Хоть проблема дублей товаров и решается тегом rel='canonical', но все же данное изменение позволяет избавиться от большинства и остается только несколько.
Теперь товар не будет доступен по таким URL:
site.ru/category/podcategory123/tovar
site.ru/category/podcategory123456/tovar
site.ru/category123/podcategory/tovar
и так далее.