diff options
author | Jesús <heckyel@hyperbola.info> | 2019-08-18 21:14:58 -0500 |
---|---|---|
committer | Jesús <heckyel@hyperbola.info> | 2019-08-18 21:14:58 -0500 |
commit | 2eed7b082f83630301e51f57ca8394de228a8605 (patch) | |
tree | 1d19962d22d30f99317d9276e4bae7744fc93fc2 /public/system/library | |
download | librecart-2eed7b082f83630301e51f57ca8394de228a8605.tar.lz librecart-2eed7b082f83630301e51f57ca8394de228a8605.tar.xz librecart-2eed7b082f83630301e51f57ca8394de228a8605.zip |
first commit
Diffstat (limited to 'public/system/library')
243 files changed, 25990 insertions, 0 deletions
diff --git a/public/system/library/cache.php b/public/system/library/cache.php new file mode 100644 index 0000000..d0aa5a7 --- /dev/null +++ b/public/system/library/cache.php @@ -0,0 +1,64 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Cache class +*/ +class Cache { + private $adaptor; + + /** + * Constructor + * + * @param string $adaptor The type of storage for the cache. + * @param int $expire Optional parameters + * + */ + public function __construct($adaptor, $expire = 3600) { + $class = 'Cache\\' . $adaptor; + + if (class_exists($class)) { + $this->adaptor = new $class($expire); + } else { + throw new \Exception('Error: Could not load cache adaptor ' . $adaptor . ' cache!'); + } + } + + /** + * Gets a cache by key name. + * + * @param string $key The cache key name + * + * @return string + */ + public function get($key) { + return $this->adaptor->get($key); + } + + /** + * + * + * @param string $key The cache key + * @param string $value The cache value + * + * @return string + */ + public function set($key, $value) { + return $this->adaptor->set($key, $value); + } + + /** + * + * + * @param string $key The cache key + */ + public function delete($key) { + return $this->adaptor->delete($key); + } +} diff --git a/public/system/library/cache/apc.php b/public/system/library/cache/apc.php new file mode 100644 index 0000000..5acd778 --- /dev/null +++ b/public/system/library/cache/apc.php @@ -0,0 +1,33 @@ +<?php +namespace Cache; +class APC { + private $expire; + private $active = false; + + public function __construct($expire) { + $this->expire = $expire; + $this->active = function_exists('apc_cache_info') && ini_get('apc.enabled'); + } + + public function get($key) { + return $this->active ? apc_fetch(CACHE_PREFIX . $key) : false; + } + + public function set($key, $value) { + return $this->active ? apc_store(CACHE_PREFIX . $key, $value, $this->expire) : false; + } + + public function delete($key) { + if (!$this->active) { + return false; + } + + $cache_info = apc_cache_info('user'); + $cache_list = $cache_info['cache_list']; + foreach ($cache_list as $entry) { + if (strpos($entry['info'], CACHE_PREFIX . $key) === 0) { + apcu_delete($entry['info']); + } + } + } +} diff --git a/public/system/library/cache/file.php b/public/system/library/cache/file.php new file mode 100644 index 0000000..0330771 --- /dev/null +++ b/public/system/library/cache/file.php @@ -0,0 +1,73 @@ +<?php +namespace Cache; +class File { + private $expire; + + public function __construct($expire = 3600) { + $this->expire = $expire; + + $files = glob(DIR_CACHE . 'cache.*'); + + if ($files) { + foreach ($files as $file) { + $time = substr(strrchr($file, '.'), 1); + + if ($time < time()) { + if (file_exists($file)) { + unlink($file); + } + } + } + } + } + + public function get($key) { + $files = glob(DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.*'); + + if ($files) { + $handle = fopen($files[0], 'r'); + + flock($handle, LOCK_SH); + + $data = fread($handle, filesize($files[0])); + + flock($handle, LOCK_UN); + + fclose($handle); + + return json_decode($data, true); + } + + return false; + } + + public function set($key, $value) { + $this->delete($key); + + $file = DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.' . (time() + $this->expire); + + $handle = fopen($file, 'w'); + + flock($handle, LOCK_EX); + + fwrite($handle, json_encode($value)); + + fflush($handle); + + flock($handle, LOCK_UN); + + fclose($handle); + } + + public function delete($key) { + $files = glob(DIR_CACHE . 'cache.' . preg_replace('/[^A-Z0-9\._-]/i', '', $key) . '.*'); + + if ($files) { + foreach ($files as $file) { + if (file_exists($file)) { + unlink($file); + } + } + } + } +}
\ No newline at end of file diff --git a/public/system/library/cache/mem.php b/public/system/library/cache/mem.php new file mode 100644 index 0000000..4e556e5 --- /dev/null +++ b/public/system/library/cache/mem.php @@ -0,0 +1,27 @@ +<?php +namespace Cache; +class Mem { + private $expire; + private $memcache; + + const CACHEDUMP_LIMIT = 9999; + + public function __construct($expire) { + $this->expire = $expire; + + $this->memcache = new \Memcache(); + $this->memcache->pconnect(CACHE_HOSTNAME, CACHE_PORT); + } + + public function get($key) { + return $this->memcache->get(CACHE_PREFIX . $key); + } + + public function set($key, $value) { + return $this->memcache->set(CACHE_PREFIX . $key, $value, MEMCACHE_COMPRESSED, $this->expire); + } + + public function delete($key) { + $this->memcache->delete(CACHE_PREFIX . $key); + } +} diff --git a/public/system/library/cache/memcached.php b/public/system/library/cache/memcached.php new file mode 100644 index 0000000..d6ae2f4 --- /dev/null +++ b/public/system/library/cache/memcached.php @@ -0,0 +1,27 @@ +<?php +namespace Cache; +class Memcached { + private $expire; + private $memcached; + + const CACHEDUMP_LIMIT = 9999; + + public function __construct($expire) { + $this->expire = $expire; + $this->memcached = new \Memcached(); + + $this->memcached->addServer(CACHE_HOSTNAME, CACHE_PORT); + } + + public function get($key) { + return $this->memcached->get(CACHE_PREFIX . $key); + } + + public function set($key, $value) { + return $this->memcached->set(CACHE_PREFIX . $key, $value, $this->expire); + } + + public function delete($key) { + $this->memcached->delete(CACHE_PREFIX . $key); + } +} diff --git a/public/system/library/cache/redis.php b/public/system/library/cache/redis.php new file mode 100644 index 0000000..315f27b --- /dev/null +++ b/public/system/library/cache/redis.php @@ -0,0 +1,30 @@ +<?php +namespace Cache; +class Redis { + private $expire; + private $cache; + + public function __construct($expire) { + $this->expire = $expire; + + $this->cache = new \Redis(); + $this->cache->pconnect(CACHE_HOSTNAME, CACHE_PORT); + } + + public function get($key) { + $data = $this->cache->get(CACHE_PREFIX . $key); + return json_decode($data, true); + } + + public function set($key,$value) { + $status = $this->cache->set(CACHE_PREFIX . $key, json_encode($value)); + if($status){ + $this->cache->setTimeout(CACHE_PREFIX . $key, $this->expire); + } + return $status; + } + + public function delete($key) { + $this->cache->delete(CACHE_PREFIX . $key); + } +}
\ No newline at end of file diff --git a/public/system/library/cart/cart.php b/public/system/library/cart/cart.php new file mode 100644 index 0000000..b283c65 --- /dev/null +++ b/public/system/library/cart/cart.php @@ -0,0 +1,407 @@ +<?php +namespace Cart; +class Cart { + private $data = array(); + + public function __construct($registry) { + $this->config = $registry->get('config'); + $this->customer = $registry->get('customer'); + $this->session = $registry->get('session'); + $this->db = $registry->get('db'); + $this->tax = $registry->get('tax'); + $this->weight = $registry->get('weight'); + + // Remove all the expired carts with no customer ID + $this->db->query("DELETE FROM " . DB_PREFIX . "cart WHERE (api_id > '0' OR customer_id = '0') AND date_added < DATE_SUB(NOW(), INTERVAL 1 HOUR)"); + + if ($this->customer->getId()) { + // We want to change the session ID on all the old items in the customers cart + $this->db->query("UPDATE " . DB_PREFIX . "cart SET session_id = '" . $this->db->escape($this->session->getId()) . "' WHERE api_id = '0' AND customer_id = '" . (int)$this->customer->getId() . "'"); + + // Once the customer is logged in we want to update the customers cart + $cart_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "cart WHERE api_id = '0' AND customer_id = '0' AND session_id = '" . $this->db->escape($this->session->getId()) . "'"); + + foreach ($cart_query->rows as $cart) { + $this->db->query("DELETE FROM " . DB_PREFIX . "cart WHERE cart_id = '" . (int)$cart['cart_id'] . "'"); + + // The advantage of using $this->add is that it will check if the products already exist and increaser the quantity if necessary. + $this->add($cart['product_id'], $cart['quantity'], json_decode($cart['option']), $cart['recurring_id']); + } + } + } + + public function getProducts() { + $product_data = array(); + + $cart_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "cart WHERE api_id = '" . (isset($this->session->data['api_id']) ? (int)$this->session->data['api_id'] : 0) . "' AND customer_id = '" . (int)$this->customer->getId() . "' AND session_id = '" . $this->db->escape($this->session->getId()) . "'"); + + foreach ($cart_query->rows as $cart) { + $stock = true; + + $product_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "product_to_store p2s LEFT JOIN " . DB_PREFIX . "product p ON (p2s.product_id = p.product_id) LEFT JOIN " . DB_PREFIX . "product_description pd ON (p.product_id = pd.product_id) WHERE p2s.store_id = '" . (int)$this->config->get('config_store_id') . "' AND p2s.product_id = '" . (int)$cart['product_id'] . "' AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "' AND p.date_available <= NOW() AND p.status = '1'"); + + if ($product_query->num_rows && ($cart['quantity'] > 0)) { + $option_price = 0; + $option_points = 0; + $option_weight = 0; + + $option_data = array(); + + foreach (json_decode($cart['option']) as $product_option_id => $value) { + $option_query = $this->db->query("SELECT po.product_option_id, po.option_id, od.name, o.type FROM " . DB_PREFIX . "product_option po LEFT JOIN `" . DB_PREFIX . "option` o ON (po.option_id = o.option_id) LEFT JOIN " . DB_PREFIX . "option_description od ON (o.option_id = od.option_id) WHERE po.product_option_id = '" . (int)$product_option_id . "' AND po.product_id = '" . (int)$cart['product_id'] . "' AND od.language_id = '" . (int)$this->config->get('config_language_id') . "'"); + + if ($option_query->num_rows) { + if ($option_query->row['type'] == 'select' || $option_query->row['type'] == 'radio') { + $option_value_query = $this->db->query("SELECT pov.option_value_id, ovd.name, pov.quantity, pov.subtract, pov.price, pov.price_prefix, pov.points, pov.points_prefix, pov.weight, pov.weight_prefix FROM " . DB_PREFIX . "product_option_value pov LEFT JOIN " . DB_PREFIX . "option_value ov ON (pov.option_value_id = ov.option_value_id) LEFT JOIN " . DB_PREFIX . "option_value_description ovd ON (ov.option_value_id = ovd.option_value_id) WHERE pov.product_option_value_id = '" . (int)$value . "' AND pov.product_option_id = '" . (int)$product_option_id . "' AND ovd.language_id = '" . (int)$this->config->get('config_language_id') . "'"); + + if ($option_value_query->num_rows) { + if ($option_value_query->row['price_prefix'] == '+') { + $option_price += $option_value_query->row['price']; + } elseif ($option_value_query->row['price_prefix'] == '-') { + $option_price -= $option_value_query->row['price']; + } + + if ($option_value_query->row['points_prefix'] == '+') { + $option_points += $option_value_query->row['points']; + } elseif ($option_value_query->row['points_prefix'] == '-') { + $option_points -= $option_value_query->row['points']; + } + + if ($option_value_query->row['weight_prefix'] == '+') { + $option_weight += $option_value_query->row['weight']; + } elseif ($option_value_query->row['weight_prefix'] == '-') { + $option_weight -= $option_value_query->row['weight']; + } + + if ($option_value_query->row['subtract'] && (!$option_value_query->row['quantity'] || ($option_value_query->row['quantity'] < $cart['quantity']))) { + $stock = false; + } + + $option_data[] = array( + 'product_option_id' => $product_option_id, + 'product_option_value_id' => $value, + 'option_id' => $option_query->row['option_id'], + 'option_value_id' => $option_value_query->row['option_value_id'], + 'name' => $option_query->row['name'], + 'value' => $option_value_query->row['name'], + 'type' => $option_query->row['type'], + 'quantity' => $option_value_query->row['quantity'], + 'subtract' => $option_value_query->row['subtract'], + 'price' => $option_value_query->row['price'], + 'price_prefix' => $option_value_query->row['price_prefix'], + 'points' => $option_value_query->row['points'], + 'points_prefix' => $option_value_query->row['points_prefix'], + 'weight' => $option_value_query->row['weight'], + 'weight_prefix' => $option_value_query->row['weight_prefix'] + ); + } + } elseif ($option_query->row['type'] == 'checkbox' && is_array($value)) { + foreach ($value as $product_option_value_id) { + $option_value_query = $this->db->query("SELECT pov.option_value_id, pov.quantity, pov.subtract, pov.price, pov.price_prefix, pov.points, pov.points_prefix, pov.weight, pov.weight_prefix, ovd.name FROM " . DB_PREFIX . "product_option_value pov LEFT JOIN " . DB_PREFIX . "option_value_description ovd ON (pov.option_value_id = ovd.option_value_id) WHERE pov.product_option_value_id = '" . (int)$product_option_value_id . "' AND pov.product_option_id = '" . (int)$product_option_id . "' AND ovd.language_id = '" . (int)$this->config->get('config_language_id') . "'"); + + if ($option_value_query->num_rows) { + if ($option_value_query->row['price_prefix'] == '+') { + $option_price += $option_value_query->row['price']; + } elseif ($option_value_query->row['price_prefix'] == '-') { + $option_price -= $option_value_query->row['price']; + } + + if ($option_value_query->row['points_prefix'] == '+') { + $option_points += $option_value_query->row['points']; + } elseif ($option_value_query->row['points_prefix'] == '-') { + $option_points -= $option_value_query->row['points']; + } + + if ($option_value_query->row['weight_prefix'] == '+') { + $option_weight += $option_value_query->row['weight']; + } elseif ($option_value_query->row['weight_prefix'] == '-') { + $option_weight -= $option_value_query->row['weight']; + } + + if ($option_value_query->row['subtract'] && (!$option_value_query->row['quantity'] || ($option_value_query->row['quantity'] < $cart['quantity']))) { + $stock = false; + } + + $option_data[] = array( + 'product_option_id' => $product_option_id, + 'product_option_value_id' => $product_option_value_id, + 'option_id' => $option_query->row['option_id'], + 'option_value_id' => $option_value_query->row['option_value_id'], + 'name' => $option_query->row['name'], + 'value' => $option_value_query->row['name'], + 'type' => $option_query->row['type'], + 'quantity' => $option_value_query->row['quantity'], + 'subtract' => $option_value_query->row['subtract'], + 'price' => $option_value_query->row['price'], + 'price_prefix' => $option_value_query->row['price_prefix'], + 'points' => $option_value_query->row['points'], + 'points_prefix' => $option_value_query->row['points_prefix'], + 'weight' => $option_value_query->row['weight'], + 'weight_prefix' => $option_value_query->row['weight_prefix'] + ); + } + } + } elseif ($option_query->row['type'] == 'text' || $option_query->row['type'] == 'textarea' || $option_query->row['type'] == 'file' || $option_query->row['type'] == 'date' || $option_query->row['type'] == 'datetime' || $option_query->row['type'] == 'time') { + $option_data[] = array( + 'product_option_id' => $product_option_id, + 'product_option_value_id' => '', + 'option_id' => $option_query->row['option_id'], + 'option_value_id' => '', + 'name' => $option_query->row['name'], + 'value' => $value, + 'type' => $option_query->row['type'], + 'quantity' => '', + 'subtract' => '', + 'price' => '', + 'price_prefix' => '', + 'points' => '', + 'points_prefix' => '', + 'weight' => '', + 'weight_prefix' => '' + ); + } + } + } + + $price = $product_query->row['price']; + + // Product Discounts + $discount_quantity = 0; + + foreach ($cart_query->rows as $cart_2) { + if ($cart_2['product_id'] == $cart['product_id']) { + $discount_quantity += $cart_2['quantity']; + } + } + + $product_discount_query = $this->db->query("SELECT price FROM " . DB_PREFIX . "product_discount WHERE product_id = '" . (int)$cart['product_id'] . "' AND customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "' AND quantity <= '" . (int)$discount_quantity . "' AND ((date_start = '0000-00-00' OR date_start < NOW()) AND (date_end = '0000-00-00' OR date_end > NOW())) ORDER BY quantity DESC, priority ASC, price ASC LIMIT 1"); + + if ($product_discount_query->num_rows) { + $price = $product_discount_query->row['price']; + } + + // Product Specials + $product_special_query = $this->db->query("SELECT price FROM " . DB_PREFIX . "product_special WHERE product_id = '" . (int)$cart['product_id'] . "' AND customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "' AND ((date_start = '0000-00-00' OR date_start < NOW()) AND (date_end = '0000-00-00' OR date_end > NOW())) ORDER BY priority ASC, price ASC LIMIT 1"); + + if ($product_special_query->num_rows) { + $price = $product_special_query->row['price']; + } + + // Reward Points + $product_reward_query = $this->db->query("SELECT points FROM " . DB_PREFIX . "product_reward WHERE product_id = '" . (int)$cart['product_id'] . "' AND customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "'"); + + if ($product_reward_query->num_rows) { + $reward = $product_reward_query->row['points']; + } else { + $reward = 0; + } + + // Downloads + $download_data = array(); + + $download_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "product_to_download p2d LEFT JOIN " . DB_PREFIX . "download d ON (p2d.download_id = d.download_id) LEFT JOIN " . DB_PREFIX . "download_description dd ON (d.download_id = dd.download_id) WHERE p2d.product_id = '" . (int)$cart['product_id'] . "' AND dd.language_id = '" . (int)$this->config->get('config_language_id') . "'"); + + foreach ($download_query->rows as $download) { + $download_data[] = array( + 'download_id' => $download['download_id'], + 'name' => $download['name'], + 'filename' => $download['filename'], + 'mask' => $download['mask'] + ); + } + + // Stock + if (!$product_query->row['quantity'] || ($product_query->row['quantity'] < $cart['quantity'])) { + $stock = false; + } + + $recurring_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "recurring r LEFT JOIN " . DB_PREFIX . "product_recurring pr ON (r.recurring_id = pr.recurring_id) LEFT JOIN " . DB_PREFIX . "recurring_description rd ON (r.recurring_id = rd.recurring_id) WHERE r.recurring_id = '" . (int)$cart['recurring_id'] . "' AND pr.product_id = '" . (int)$cart['product_id'] . "' AND rd.language_id = " . (int)$this->config->get('config_language_id') . " AND r.status = 1 AND pr.customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "'"); + + if ($recurring_query->num_rows) { + $recurring = array( + 'recurring_id' => $cart['recurring_id'], + 'name' => $recurring_query->row['name'], + 'frequency' => $recurring_query->row['frequency'], + 'price' => $recurring_query->row['price'], + 'cycle' => $recurring_query->row['cycle'], + 'duration' => $recurring_query->row['duration'], + 'trial' => $recurring_query->row['trial_status'], + 'trial_frequency' => $recurring_query->row['trial_frequency'], + 'trial_price' => $recurring_query->row['trial_price'], + 'trial_cycle' => $recurring_query->row['trial_cycle'], + 'trial_duration' => $recurring_query->row['trial_duration'] + ); + } else { + $recurring = false; + } + + $product_data[] = array( + 'cart_id' => $cart['cart_id'], + 'product_id' => $product_query->row['product_id'], + 'name' => $product_query->row['name'], + 'model' => $product_query->row['model'], + 'shipping' => $product_query->row['shipping'], + 'image' => $product_query->row['image'], + 'option' => $option_data, + 'download' => $download_data, + 'quantity' => $cart['quantity'], + 'minimum' => $product_query->row['minimum'], + 'subtract' => $product_query->row['subtract'], + 'stock' => $stock, + 'price' => ($price + $option_price), + 'total' => ($price + $option_price) * $cart['quantity'], + 'reward' => $reward * $cart['quantity'], + 'points' => ($product_query->row['points'] ? ($product_query->row['points'] + $option_points) * $cart['quantity'] : 0), + 'tax_class_id' => $product_query->row['tax_class_id'], + 'weight' => ($product_query->row['weight'] + $option_weight) * $cart['quantity'], + 'weight_class_id' => $product_query->row['weight_class_id'], + 'length' => $product_query->row['length'], + 'width' => $product_query->row['width'], + 'height' => $product_query->row['height'], + 'length_class_id' => $product_query->row['length_class_id'], + 'recurring' => $recurring + ); + } else { + $this->remove($cart['cart_id']); + } + } + + return $product_data; + } + + public function add($product_id, $quantity = 1, $option = array(), $recurring_id = 0) { + $query = $this->db->query("SELECT COUNT(*) AS total FROM " . DB_PREFIX . "cart WHERE api_id = '" . (isset($this->session->data['api_id']) ? (int)$this->session->data['api_id'] : 0) . "' AND customer_id = '" . (int)$this->customer->getId() . "' AND session_id = '" . $this->db->escape($this->session->getId()) . "' AND product_id = '" . (int)$product_id . "' AND recurring_id = '" . (int)$recurring_id . "' AND `option` = '" . $this->db->escape(json_encode($option)) . "'"); + + if (!$query->row['total']) { + $this->db->query("INSERT " . DB_PREFIX . "cart SET api_id = '" . (isset($this->session->data['api_id']) ? (int)$this->session->data['api_id'] : 0) . "', customer_id = '" . (int)$this->customer->getId() . "', session_id = '" . $this->db->escape($this->session->getId()) . "', product_id = '" . (int)$product_id . "', recurring_id = '" . (int)$recurring_id . "', `option` = '" . $this->db->escape(json_encode($option)) . "', quantity = '" . (int)$quantity . "', date_added = NOW()"); + } else { + $this->db->query("UPDATE " . DB_PREFIX . "cart SET quantity = (quantity + " . (int)$quantity . ") WHERE api_id = '" . (isset($this->session->data['api_id']) ? (int)$this->session->data['api_id'] : 0) . "' AND customer_id = '" . (int)$this->customer->getId() . "' AND session_id = '" . $this->db->escape($this->session->getId()) . "' AND product_id = '" . (int)$product_id . "' AND recurring_id = '" . (int)$recurring_id . "' AND `option` = '" . $this->db->escape(json_encode($option)) . "'"); + } + } + + public function update($cart_id, $quantity) { + $this->db->query("UPDATE " . DB_PREFIX . "cart SET quantity = '" . (int)$quantity . "' WHERE cart_id = '" . (int)$cart_id . "' AND api_id = '" . (isset($this->session->data['api_id']) ? (int)$this->session->data['api_id'] : 0) . "' AND customer_id = '" . (int)$this->customer->getId() . "' AND session_id = '" . $this->db->escape($this->session->getId()) . "'"); + } + + public function remove($cart_id) { + $this->db->query("DELETE FROM " . DB_PREFIX . "cart WHERE cart_id = '" . (int)$cart_id . "' AND api_id = '" . (isset($this->session->data['api_id']) ? (int)$this->session->data['api_id'] : 0) . "' AND customer_id = '" . (int)$this->customer->getId() . "' AND session_id = '" . $this->db->escape($this->session->getId()) . "'"); + } + + public function clear() { + $this->db->query("DELETE FROM " . DB_PREFIX . "cart WHERE api_id = '" . (isset($this->session->data['api_id']) ? (int)$this->session->data['api_id'] : 0) . "' AND customer_id = '" . (int)$this->customer->getId() . "' AND session_id = '" . $this->db->escape($this->session->getId()) . "'"); + } + + public function getRecurringProducts() { + $product_data = array(); + + foreach ($this->getProducts() as $value) { + if ($value['recurring']) { + $product_data[] = $value; + } + } + + return $product_data; + } + + public function getWeight() { + $weight = 0; + + foreach ($this->getProducts() as $product) { + if ($product['shipping']) { + $weight += $this->weight->convert($product['weight'], $product['weight_class_id'], $this->config->get('config_weight_class_id')); + } + } + + return $weight; + } + + public function getSubTotal() { + $total = 0; + + foreach ($this->getProducts() as $product) { + $total += $product['total']; + } + + return $total; + } + + public function getTaxes() { + $tax_data = array(); + + foreach ($this->getProducts() as $product) { + if ($product['tax_class_id']) { + $tax_rates = $this->tax->getRates($product['price'], $product['tax_class_id']); + + foreach ($tax_rates as $tax_rate) { + if (!isset($tax_data[$tax_rate['tax_rate_id']])) { + $tax_data[$tax_rate['tax_rate_id']] = ($tax_rate['amount'] * $product['quantity']); + } else { + $tax_data[$tax_rate['tax_rate_id']] += ($tax_rate['amount'] * $product['quantity']); + } + } + } + } + + return $tax_data; + } + + public function getTotal() { + $total = 0; + + foreach ($this->getProducts() as $product) { + $total += $this->tax->calculate($product['price'], $product['tax_class_id'], $this->config->get('config_tax')) * $product['quantity']; + } + + return $total; + } + + public function countProducts() { + $product_total = 0; + + $products = $this->getProducts(); + + foreach ($products as $product) { + $product_total += $product['quantity']; + } + + return $product_total; + } + + public function hasProducts() { + return count($this->getProducts()); + } + + public function hasRecurringProducts() { + return count($this->getRecurringProducts()); + } + + public function hasStock() { + foreach ($this->getProducts() as $product) { + if (!$product['stock']) { + return false; + } + } + + return true; + } + + public function hasShipping() { + foreach ($this->getProducts() as $product) { + if ($product['shipping']) { + return true; + } + } + + return false; + } + + public function hasDownload() { + foreach ($this->getProducts() as $product) { + if ($product['download']) { + return true; + } + } + + return false; + } +} diff --git a/public/system/library/cart/currency.php b/public/system/library/cart/currency.php new file mode 100644 index 0000000..02026bb --- /dev/null +++ b/public/system/library/cart/currency.php @@ -0,0 +1,115 @@ +<?php +namespace Cart; +class Currency { + private $currencies = array(); + + public function __construct($registry) { + $this->db = $registry->get('db'); + $this->language = $registry->get('language'); + + $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "currency"); + + foreach ($query->rows as $result) { + $this->currencies[$result['code']] = array( + 'currency_id' => $result['currency_id'], + 'title' => $result['title'], + 'symbol_left' => $result['symbol_left'], + 'symbol_right' => $result['symbol_right'], + 'decimal_place' => $result['decimal_place'], + 'value' => $result['value'] + ); + } + } + + public function format($number, $currency, $value = '', $format = true) { + $symbol_left = $this->currencies[$currency]['symbol_left']; + $symbol_right = $this->currencies[$currency]['symbol_right']; + $decimal_place = $this->currencies[$currency]['decimal_place']; + + if (!$value) { + $value = $this->currencies[$currency]['value']; + } + + $amount = $value ? (float)$number * $value : (float)$number; + + $amount = round($amount, (int)$decimal_place); + + if (!$format) { + return $amount; + } + + $string = ''; + + if ($symbol_left) { + $string .= $symbol_left; + } + + $string .= number_format($amount, (int)$decimal_place, $this->language->get('decimal_point'), $this->language->get('thousand_point')); + + if ($symbol_right) { + $string .= $symbol_right; + } + + return $string; + } + + public function convert($value, $from, $to) { + if (isset($this->currencies[$from])) { + $from = $this->currencies[$from]['value']; + } else { + $from = 1; + } + + if (isset($this->currencies[$to])) { + $to = $this->currencies[$to]['value']; + } else { + $to = 1; + } + + return $value * ($to / $from); + } + + public function getId($currency) { + if (isset($this->currencies[$currency])) { + return $this->currencies[$currency]['currency_id']; + } else { + return 0; + } + } + + public function getSymbolLeft($currency) { + if (isset($this->currencies[$currency])) { + return $this->currencies[$currency]['symbol_left']; + } else { + return ''; + } + } + + public function getSymbolRight($currency) { + if (isset($this->currencies[$currency])) { + return $this->currencies[$currency]['symbol_right']; + } else { + return ''; + } + } + + public function getDecimalPlace($currency) { + if (isset($this->currencies[$currency])) { + return $this->currencies[$currency]['decimal_place']; + } else { + return 0; + } + } + + public function getValue($currency) { + if (isset($this->currencies[$currency])) { + return $this->currencies[$currency]['value']; + } else { + return 0; + } + } + + public function has($currency) { + return isset($this->currencies[$currency]); + } +} diff --git a/public/system/library/cart/customer.php b/public/system/library/cart/customer.php new file mode 100644 index 0000000..c3de558 --- /dev/null +++ b/public/system/library/cart/customer.php @@ -0,0 +1,132 @@ +<?php +namespace Cart; +class Customer { + private $customer_id; + private $firstname; + private $lastname; + private $customer_group_id; + private $email; + private $telephone; + private $newsletter; + private $address_id; + + public function __construct($registry) { + $this->config = $registry->get('config'); + $this->db = $registry->get('db'); + $this->request = $registry->get('request'); + $this->session = $registry->get('session'); + + if (isset($this->session->data['customer_id'])) { + $customer_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "customer WHERE customer_id = '" . (int)$this->session->data['customer_id'] . "' AND status = '1'"); + + if ($customer_query->num_rows) { + $this->customer_id = $customer_query->row['customer_id']; + $this->firstname = $customer_query->row['firstname']; + $this->lastname = $customer_query->row['lastname']; + $this->customer_group_id = $customer_query->row['customer_group_id']; + $this->email = $customer_query->row['email']; + $this->telephone = $customer_query->row['telephone']; + $this->newsletter = $customer_query->row['newsletter']; + $this->address_id = $customer_query->row['address_id']; + + $this->db->query("UPDATE " . DB_PREFIX . "customer SET language_id = '" . (int)$this->config->get('config_language_id') . "', ip = '" . $this->db->escape($this->request->server['REMOTE_ADDR']) . "' WHERE customer_id = '" . (int)$this->customer_id . "'"); + + $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "customer_ip WHERE customer_id = '" . (int)$this->session->data['customer_id'] . "' AND ip = '" . $this->db->escape($this->request->server['REMOTE_ADDR']) . "'"); + + if (!$query->num_rows) { + $this->db->query("INSERT INTO " . DB_PREFIX . "customer_ip SET customer_id = '" . (int)$this->session->data['customer_id'] . "', ip = '" . $this->db->escape($this->request->server['REMOTE_ADDR']) . "', date_added = NOW()"); + } + } else { + $this->logout(); + } + } + } + + public function login($email, $password, $override = false) { + if ($override) { + $customer_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "customer WHERE LOWER(email) = '" . $this->db->escape(utf8_strtolower($email)) . "' AND status = '1'"); + } else { + $customer_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "customer WHERE LOWER(email) = '" . $this->db->escape(utf8_strtolower($email)) . "' AND (password = SHA1(CONCAT(salt, SHA1(CONCAT(salt, SHA1('" . $this->db->escape($password) . "'))))) OR password = '" . $this->db->escape(md5($password)) . "') AND status = '1'"); + } + + if ($customer_query->num_rows) { + $this->session->data['customer_id'] = $customer_query->row['customer_id']; + + $this->customer_id = $customer_query->row['customer_id']; + $this->firstname = $customer_query->row['firstname']; + $this->lastname = $customer_query->row['lastname']; + $this->customer_group_id = $customer_query->row['customer_group_id']; + $this->email = $customer_query->row['email']; + $this->telephone = $customer_query->row['telephone']; + $this->newsletter = $customer_query->row['newsletter']; + $this->address_id = $customer_query->row['address_id']; + + $this->db->query("UPDATE " . DB_PREFIX . "customer SET language_id = '" . (int)$this->config->get('config_language_id') . "', ip = '" . $this->db->escape($this->request->server['REMOTE_ADDR']) . "' WHERE customer_id = '" . (int)$this->customer_id . "'"); + + return true; + } else { + return false; + } + } + + public function logout() { + unset($this->session->data['customer_id']); + + $this->customer_id = ''; + $this->firstname = ''; + $this->lastname = ''; + $this->customer_group_id = ''; + $this->email = ''; + $this->telephone = ''; + $this->newsletter = ''; + $this->address_id = ''; + } + + public function isLogged() { + return $this->customer_id; + } + + public function getId() { + return $this->customer_id; + } + + public function getFirstName() { + return $this->firstname; + } + + public function getLastName() { + return $this->lastname; + } + + public function getGroupId() { + return $this->customer_group_id; + } + + public function getEmail() { + return $this->email; + } + + public function getTelephone() { + return $this->telephone; + } + + public function getNewsletter() { + return $this->newsletter; + } + + public function getAddressId() { + return $this->address_id; + } + + public function getBalance() { + $query = $this->db->query("SELECT SUM(amount) AS total FROM " . DB_PREFIX . "customer_transaction WHERE customer_id = '" . (int)$this->customer_id . "'"); + + return $query->row['total']; + } + + public function getRewardPoints() { + $query = $this->db->query("SELECT SUM(points) AS total FROM " . DB_PREFIX . "customer_reward WHERE customer_id = '" . (int)$this->customer_id . "'"); + + return $query->row['total']; + } +} diff --git a/public/system/library/cart/length.php b/public/system/library/cart/length.php new file mode 100644 index 0000000..65af015 --- /dev/null +++ b/public/system/library/cart/length.php @@ -0,0 +1,57 @@ +<?php +namespace Cart; +class Length { + private $lengths = array(); + + public function __construct($registry) { + $this->db = $registry->get('db'); + $this->config = $registry->get('config'); + + $length_class_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "length_class mc LEFT JOIN " . DB_PREFIX . "length_class_description mcd ON (mc.length_class_id = mcd.length_class_id) WHERE mcd.language_id = '" . (int)$this->config->get('config_language_id') . "'"); + + foreach ($length_class_query->rows as $result) { + $this->lengths[$result['length_class_id']] = array( + 'length_class_id' => $result['length_class_id'], + 'title' => $result['title'], + 'unit' => $result['unit'], + 'value' => $result['value'] + ); + } + } + + public function convert($value, $from, $to) { + if ($from == $to) { + return $value; + } + + if (isset($this->lengths[$from])) { + $from = $this->lengths[$from]['value']; + } else { + $from = 1; + } + + if (isset($this->lengths[$to])) { + $to = $this->lengths[$to]['value']; + } else { + $to = 1; + } + + return $value * ($to / $from); + } + + public function format($value, $length_class_id, $decimal_point = '.', $thousand_point = ',') { + if (isset($this->lengths[$length_class_id])) { + return number_format($value, 2, $decimal_point, $thousand_point) . $this->lengths[$length_class_id]['unit']; + } else { + return number_format($value, 2, $decimal_point, $thousand_point); + } + } + + public function getUnit($length_class_id) { + if (isset($this->lengths[$length_class_id])) { + return $this->lengths[$length_class_id]['unit']; + } else { + return ''; + } + } +} diff --git a/public/system/library/cart/tax.php b/public/system/library/cart/tax.php new file mode 100644 index 0000000..d535c23 --- /dev/null +++ b/public/system/library/cart/tax.php @@ -0,0 +1,128 @@ +<?php +namespace Cart; +final class Tax { + private $tax_rates = array(); + + public function __construct($registry) { + $this->config = $registry->get('config'); + $this->db = $registry->get('db'); + } + + public function unsetRates() { + $this->tax_rates = array(); + } + + public function setShippingAddress($country_id, $zone_id) { + $tax_query = $this->db->query("SELECT tr1.tax_class_id, tr2.tax_rate_id, tr2.name, tr2.rate, tr2.type, tr1.priority FROM " . DB_PREFIX . "tax_rule tr1 LEFT JOIN " . DB_PREFIX . "tax_rate tr2 ON (tr1.tax_rate_id = tr2.tax_rate_id) INNER JOIN " . DB_PREFIX . "tax_rate_to_customer_group tr2cg ON (tr2.tax_rate_id = tr2cg.tax_rate_id) LEFT JOIN " . DB_PREFIX . "zone_to_geo_zone z2gz ON (tr2.geo_zone_id = z2gz.geo_zone_id) LEFT JOIN " . DB_PREFIX . "geo_zone gz ON (tr2.geo_zone_id = gz.geo_zone_id) WHERE tr1.based = 'shipping' AND tr2cg.customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "' AND z2gz.country_id = '" . (int)$country_id . "' AND (z2gz.zone_id = '0' OR z2gz.zone_id = '" . (int)$zone_id . "') ORDER BY tr1.priority ASC"); + + foreach ($tax_query->rows as $result) { + $this->tax_rates[$result['tax_class_id']][$result['tax_rate_id']] = array( + 'tax_rate_id' => $result['tax_rate_id'], + 'name' => $result['name'], + 'rate' => $result['rate'], + 'type' => $result['type'], + 'priority' => $result['priority'] + ); + } + } + + public function setPaymentAddress($country_id, $zone_id) { + $tax_query = $this->db->query("SELECT tr1.tax_class_id, tr2.tax_rate_id, tr2.name, tr2.rate, tr2.type, tr1.priority FROM " . DB_PREFIX . "tax_rule tr1 LEFT JOIN " . DB_PREFIX . "tax_rate tr2 ON (tr1.tax_rate_id = tr2.tax_rate_id) INNER JOIN " . DB_PREFIX . "tax_rate_to_customer_group tr2cg ON (tr2.tax_rate_id = tr2cg.tax_rate_id) LEFT JOIN " . DB_PREFIX . "zone_to_geo_zone z2gz ON (tr2.geo_zone_id = z2gz.geo_zone_id) LEFT JOIN " . DB_PREFIX . "geo_zone gz ON (tr2.geo_zone_id = gz.geo_zone_id) WHERE tr1.based = 'payment' AND tr2cg.customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "' AND z2gz.country_id = '" . (int)$country_id . "' AND (z2gz.zone_id = '0' OR z2gz.zone_id = '" . (int)$zone_id . "') ORDER BY tr1.priority ASC"); + + foreach ($tax_query->rows as $result) { + $this->tax_rates[$result['tax_class_id']][$result['tax_rate_id']] = array( + 'tax_rate_id' => $result['tax_rate_id'], + 'name' => $result['name'], + 'rate' => $result['rate'], + 'type' => $result['type'], + 'priority' => $result['priority'] + ); + } + } + + public function setStoreAddress($country_id, $zone_id) { + $tax_query = $this->db->query("SELECT tr1.tax_class_id, tr2.tax_rate_id, tr2.name, tr2.rate, tr2.type, tr1.priority FROM " . DB_PREFIX . "tax_rule tr1 LEFT JOIN " . DB_PREFIX . "tax_rate tr2 ON (tr1.tax_rate_id = tr2.tax_rate_id) INNER JOIN " . DB_PREFIX . "tax_rate_to_customer_group tr2cg ON (tr2.tax_rate_id = tr2cg.tax_rate_id) LEFT JOIN " . DB_PREFIX . "zone_to_geo_zone z2gz ON (tr2.geo_zone_id = z2gz.geo_zone_id) LEFT JOIN " . DB_PREFIX . "geo_zone gz ON (tr2.geo_zone_id = gz.geo_zone_id) WHERE tr1.based = 'store' AND tr2cg.customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "' AND z2gz.country_id = '" . (int)$country_id . "' AND (z2gz.zone_id = '0' OR z2gz.zone_id = '" . (int)$zone_id . "') ORDER BY tr1.priority ASC"); + + foreach ($tax_query->rows as $result) { + $this->tax_rates[$result['tax_class_id']][$result['tax_rate_id']] = array( + 'tax_rate_id' => $result['tax_rate_id'], + 'name' => $result['name'], + 'rate' => $result['rate'], + 'type' => $result['type'], + 'priority' => $result['priority'] + ); + } + } + + public function calculate($value, $tax_class_id, $calculate = true) { + if ($tax_class_id && $calculate) { + $amount = 0; + + $tax_rates = $this->getRates($value, $tax_class_id); + + foreach ($tax_rates as $tax_rate) { + if ($calculate != 'P' && $calculate != 'F') { + $amount += $tax_rate['amount']; + } elseif ($tax_rate['type'] == $calculate) { + $amount += $tax_rate['amount']; + } + } + + return $value + $amount; + } else { + return $value; + } + } + + public function getTax($value, $tax_class_id) { + $amount = 0; + + $tax_rates = $this->getRates($value, $tax_class_id); + + foreach ($tax_rates as $tax_rate) { + $amount += $tax_rate['amount']; + } + + return $amount; + } + + public function getRateName($tax_rate_id) { + $tax_query = $this->db->query("SELECT name FROM " . DB_PREFIX . "tax_rate WHERE tax_rate_id = '" . (int)$tax_rate_id . "'"); + + if ($tax_query->num_rows) { + return $tax_query->row['name']; + } else { + return false; + } + } + + public function getRates($value, $tax_class_id) { + $tax_rate_data = array(); + + if (isset($this->tax_rates[$tax_class_id])) { + foreach ($this->tax_rates[$tax_class_id] as $tax_rate) { + if (isset($tax_rate_data[$tax_rate['tax_rate_id']])) { + $amount = $tax_rate_data[$tax_rate['tax_rate_id']]['amount']; + } else { + $amount = 0; + } + + if ($tax_rate['type'] == 'F') { + $amount += $tax_rate['rate']; + } elseif ($tax_rate['type'] == 'P') { + $amount += ($value / 100 * $tax_rate['rate']); + } + + $tax_rate_data[$tax_rate['tax_rate_id']] = array( + 'tax_rate_id' => $tax_rate['tax_rate_id'], + 'name' => $tax_rate['name'], + 'rate' => $tax_rate['rate'], + 'type' => $tax_rate['type'], + 'amount' => $amount + ); + } + } + + return $tax_rate_data; + } +} diff --git a/public/system/library/cart/user.php b/public/system/library/cart/user.php new file mode 100644 index 0000000..ca1d09e --- /dev/null +++ b/public/system/library/cart/user.php @@ -0,0 +1,95 @@ +<?php +namespace Cart; +class User { + private $user_id; + private $user_group_id; + private $username; + private $permission = array(); + + public function __construct($registry) { + $this->db = $registry->get('db'); + $this->request = $registry->get('request'); + $this->session = $registry->get('session'); + + if (isset($this->session->data['user_id'])) { + $user_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "user WHERE user_id = '" . (int)$this->session->data['user_id'] . "' AND status = '1'"); + + if ($user_query->num_rows) { + $this->user_id = $user_query->row['user_id']; + $this->username = $user_query->row['username']; + $this->user_group_id = $user_query->row['user_group_id']; + + $this->db->query("UPDATE " . DB_PREFIX . "user SET ip = '" . $this->db->escape($this->request->server['REMOTE_ADDR']) . "' WHERE user_id = '" . (int)$this->session->data['user_id'] . "'"); + + $user_group_query = $this->db->query("SELECT permission FROM " . DB_PREFIX . "user_group WHERE user_group_id = '" . (int)$user_query->row['user_group_id'] . "'"); + + $permissions = json_decode($user_group_query->row['permission'], true); + + if (is_array($permissions)) { + foreach ($permissions as $key => $value) { + $this->permission[$key] = $value; + } + } + } else { + $this->logout(); + } + } + } + + public function login($username, $password) { + $user_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "user WHERE username = '" . $this->db->escape($username) . "' AND (password = SHA1(CONCAT(salt, SHA1(CONCAT(salt, SHA1('" . $this->db->escape($password) . "'))))) OR password = '" . $this->db->escape(md5($password)) . "') AND status = '1'"); + + if ($user_query->num_rows) { + $this->session->data['user_id'] = $user_query->row['user_id']; + + $this->user_id = $user_query->row['user_id']; + $this->username = $user_query->row['username']; + $this->user_group_id = $user_query->row['user_group_id']; + + $user_group_query = $this->db->query("SELECT permission FROM " . DB_PREFIX . "user_group WHERE user_group_id = '" . (int)$user_query->row['user_group_id'] . "'"); + + $permissions = json_decode($user_group_query->row['permission'], true); + + if (is_array($permissions)) { + foreach ($permissions as $key => $value) { + $this->permission[$key] = $value; + } + } + + return true; + } else { + return false; + } + } + + public function logout() { + unset($this->session->data['user_id']); + + $this->user_id = ''; + $this->username = ''; + } + + public function hasPermission($key, $value) { + if (isset($this->permission[$key])) { + return in_array($value, $this->permission[$key]); + } else { + return false; + } + } + + public function isLogged() { + return $this->user_id; + } + + public function getId() { + return $this->user_id; + } + + public function getUserName() { + return $this->username; + } + + public function getGroupId() { + return $this->user_group_id; + } +}
\ No newline at end of file diff --git a/public/system/library/cart/weight.php b/public/system/library/cart/weight.php new file mode 100644 index 0000000..a0df809 --- /dev/null +++ b/public/system/library/cart/weight.php @@ -0,0 +1,57 @@ +<?php +namespace Cart; +class Weight { + private $weights = array(); + + public function __construct($registry) { + $this->db = $registry->get('db'); + $this->config = $registry->get('config'); + + $weight_class_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "weight_class wc LEFT JOIN " . DB_PREFIX . "weight_class_description wcd ON (wc.weight_class_id = wcd.weight_class_id) WHERE wcd.language_id = '" . (int)$this->config->get('config_language_id') . "'"); + + foreach ($weight_class_query->rows as $result) { + $this->weights[$result['weight_class_id']] = array( + 'weight_class_id' => $result['weight_class_id'], + 'title' => $result['title'], + 'unit' => $result['unit'], + 'value' => $result['value'] + ); + } + } + + public function convert($value, $from, $to) { + if ($from == $to) { + return $value; + } + + if (isset($this->weights[$from])) { + $from = $this->weights[$from]['value']; + } else { + $from = 1; + } + + if (isset($this->weights[$to])) { + $to = $this->weights[$to]['value']; + } else { + $to = 1; + } + + return $value * ($to / $from); + } + + public function format($value, $weight_class_id, $decimal_point = '.', $thousand_point = ',') { + if (isset($this->weights[$weight_class_id])) { + return number_format($value, 2, $decimal_point, $thousand_point) . $this->weights[$weight_class_id]['unit']; + } else { + return number_format($value, 2, $decimal_point, $thousand_point); + } + } + + public function getUnit($weight_class_id) { + if (isset($this->weights[$weight_class_id])) { + return $this->weights[$weight_class_id]['unit']; + } else { + return ''; + } + } +}
\ No newline at end of file diff --git a/public/system/library/config.php b/public/system/library/config.php new file mode 100644 index 0000000..a81c16e --- /dev/null +++ b/public/system/library/config.php @@ -0,0 +1,67 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Config class +*/ +class Config { + private $data = array(); + + /** + * + * + * @param string $key + * + * @return mixed + */ + public function get($key) { + return (isset($this->data[$key]) ? $this->data[$key] : null); + } + + /** + * + * + * @param string $key + * @param string $value + */ + public function set($key, $value) { + $this->data[$key] = $value; + } + + /** + * + * + * @param string $key + * + * @return mixed + */ + public function has($key) { + return isset($this->data[$key]); + } + + /** + * + * + * @param string $filename + */ + public function load($filename) { + $file = DIR_CONFIG . $filename . '.php'; + + if (file_exists($file)) { + $_ = array(); + + require($file); + + $this->data = array_merge($this->data, $_); + } else { + trigger_error('Error: Could not load config ' . $filename . '!'); + exit(); + } + } +}
\ No newline at end of file diff --git a/public/system/library/db.php b/public/system/library/db.php new file mode 100644 index 0000000..f2febac --- /dev/null +++ b/public/system/library/db.php @@ -0,0 +1,85 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* DB class +*/ +class DB { + private $adaptor; + + /** + * Constructor + * + * @param string $adaptor + * @param string $hostname + * @param string $username + * @param string $password + * @param string $database + * @param int $port + * + */ + public function __construct($adaptor, $hostname, $username, $password, $database, $port = NULL) { + $class = 'DB\\' . $adaptor; + + if (class_exists($class)) { + $this->adaptor = new $class($hostname, $username, $password, $database, $port); + } else { + throw new \Exception('Error: Could not load database adaptor ' . $adaptor . '!'); + } + } + + /** + * + * + * @param string $sql + * + * @return array + */ + public function query($sql) { + return $this->adaptor->query($sql); + } + + /** + * + * + * @param string $value + * + * @return string + */ + public function escape($value) { + return $this->adaptor->escape($value); + } + + /** + * + * + * @return int + */ + public function countAffected() { + return $this->adaptor->countAffected(); + } + + /** + * + * + * @return int + */ + public function getLastId() { + return $this->adaptor->getLastId(); + } + + /** + * + * + * @return bool + */ + public function connected() { + return $this->adaptor->connected(); + } +}
\ No newline at end of file diff --git a/public/system/library/db/mpdo.php b/public/system/library/db/mpdo.php new file mode 100644 index 0000000..1cdb0ea --- /dev/null +++ b/public/system/library/db/mpdo.php @@ -0,0 +1,111 @@ +<?php +namespace DB; +final class mPDO { + private $connection = null; + private $statement = null; + + public function __construct($hostname, $username, $password, $database, $port = '3306') { + try { + $this->connection = new \PDO("mysql:host=" . $hostname . ";port=" . $port . ";dbname=" . $database, $username, $password, array(\PDO::ATTR_PERSISTENT => true)); + } catch(\PDOException $e) { + throw new \Exception('Failed to connect to database. Reason: \'' . $e->getMessage() . '\''); + } + + $this->connection->exec("SET NAMES 'utf8'"); + $this->connection->exec("SET CHARACTER SET utf8"); + $this->connection->exec("SET CHARACTER_SET_CONNECTION=utf8"); + $this->connection->exec("SET SQL_MODE = ''"); + } + + public function prepare($sql) { + $this->statement = $this->connection->prepare($sql); + } + + public function bindParam($parameter, $variable, $data_type = \PDO::PARAM_STR, $length = 0) { + if ($length) { + $this->statement->bindParam($parameter, $variable, $data_type, $length); + } else { + $this->statement->bindParam($parameter, $variable, $data_type); + } + } + + public function execute() { + try { + if ($this->statement && $this->statement->execute()) { + $data = array(); + + while ($row = $this->statement->fetch(\PDO::FETCH_ASSOC)) { + $data[] = $row; + } + + $result = new \stdClass(); + $result->row = (isset($data[0])) ? $data[0] : array(); + $result->rows = $data; + $result->num_rows = $this->statement->rowCount(); + } + } catch(\PDOException $e) { + throw new \Exception('Error: ' . $e->getMessage() . ' Error Code : ' . $e->getCode()); + } + } + + public function query($sql, $params = array()) { + $this->statement = $this->connection->prepare($sql); + + $result = false; + + try { + if ($this->statement && $this->statement->execute($params)) { + $data = array(); + + while ($row = $this->statement->fetch(\PDO::FETCH_ASSOC)) { + $data[] = $row; + } + + $result = new \stdClass(); + $result->row = (isset($data[0]) ? $data[0] : array()); + $result->rows = $data; + $result->num_rows = $this->statement->rowCount(); + } + } catch (\PDOException $e) { + throw new \Exception('Error: ' . $e->getMessage() . ' Error Code : ' . $e->getCode() . ' <br />' . $sql); + } + + if ($result) { + return $result; + } else { + $result = new \stdClass(); + $result->row = array(); + $result->rows = array(); + $result->num_rows = 0; + return $result; + } + } + + public function escape($value) { + return str_replace(array("\\", "\0", "\n", "\r", "\x1a", "'", '"'), array("\\\\", "\\0", "\\n", "\\r", "\Z", "\'", '\"'), $value); + } + + public function countAffected() { + if ($this->statement) { + return $this->statement->rowCount(); + } else { + return 0; + } + } + + public function getLastId() { + return $this->connection->lastInsertId(); + } + + public function isConnected() { + if ($this->connection) { + return true; + } else { + return false; + } + } + + public function __destruct() { + $this->connection = null; + } +} diff --git a/public/system/library/db/mssql.php b/public/system/library/db/mssql.php new file mode 100644 index 0000000..06d3c78 --- /dev/null +++ b/public/system/library/db/mssql.php @@ -0,0 +1,79 @@ +<?php +namespace DB; +final class MSSQL { + private $connection; + + public function __construct($hostname, $username, $password, $database, $port = '1433') { + if (!$this->connection = mssql_connect($hostname. ':' . $port, $username, $password)) { + throw new \Exception('Error: Could not make a database connection using ' . $username . '@' . $hostname); + } + + if (!mssql_select_db($database, $this->link)) { + throw new \Exception('Error: Could not connect to database ' . $database); + } + + mssql_query("SET NAMES 'utf8'", $this->connection); + mssql_query("SET CHARACTER SET utf8", $this->connection); + } + + public function query($sql) { + $resource = mssql_query($sql, $this->connection); + + if ($resource) { + if (is_resource($resource)) { + $i = 0; + + $data = array(); + + while ($result = mssql_fetch_assoc($resource)) { + $data[$i] = $result; + + $i++; + } + + mssql_free_result($resource); + + $query = new \stdClass(); + $query->row = isset($data[0]) ? $data[0] : array(); + $query->rows = $data; + $query->num_rows = $i; + + unset($data); + + return $query; + } else { + return true; + } + } else { + throw new \Exception('Error: ' . mssql_get_last_message($this->connection) . '<br />' . $sql); + } + } + + public function escape($value) { + $unpacked = unpack('H*hex', $value); + + return '0x' . $unpacked['hex']; + } + + public function countAffected() { + return mssql_rows_affected($this->connection); + } + + public function getLastId() { + $last_id = false; + + $resource = mssql_query("SELECT @@identity AS id", $this->connection); + + if ($row = mssql_fetch_row($resource)) { + $last_id = trim($row[0]); + } + + mssql_free_result($resource); + + return $last_id; + } + + public function __destruct() { + mssql_close($this->connection); + } +}
\ No newline at end of file diff --git a/public/system/library/db/mysql.php b/public/system/library/db/mysql.php new file mode 100644 index 0000000..8f6c259 --- /dev/null +++ b/public/system/library/db/mysql.php @@ -0,0 +1,90 @@ +<?php +namespace DB; +final class MySQL { + private $connection; + + public function __construct($hostname, $username, $password, $database, $port = '3306') { + if (!$this->connection = mysql_connect($hostname . ':' . $port, $username, $password)) { + trigger_error('Error: Could not make a database link using ' . $username . '@' . $hostname); + exit(); + } + + if (!mysql_select_db($database, $this->connection)) { + throw new \Exception('Error: Could not connect to database ' . $database); + } + + mysql_query("SET NAMES 'utf8'", $this->connection); + mysql_query("SET CHARACTER SET utf8", $this->connection); + mysql_query("SET CHARACTER_SET_CONNECTION=utf8", $this->connection); + mysql_query("SET SQL_MODE = ''", $this->connection); + } + + public function query($sql) { + if ($this->connection) { + $resource = mysql_query($sql, $this->connection); + + if ($resource) { + if (is_resource($resource)) { + $i = 0; + + $data = array(); + + while ($result = mysql_fetch_assoc($resource)) { + $data[$i] = $result; + + $i++; + } + + mysql_free_result($resource); + + $query = new \stdClass(); + $query->row = isset($data[0]) ? $data[0] : array(); + $query->rows = $data; + $query->num_rows = $i; + + unset($data); + + return $query; + } else { + return true; + } + } else { + $trace = debug_backtrace(); + + throw new \Exception('Error: ' . mysql_error($this->connection) . '<br />Error No: ' . mysql_errno($this->connection) . '<br /> Error in: <b>' . $trace[1]['file'] . '</b> line <b>' . $trace[1]['line'] . '</b><br />' . $sql); + } + } + } + + public function escape($value) { + if ($this->connection) { + return mysql_real_escape_string($value, $this->connection); + } + } + + public function countAffected() { + if ($this->connection) { + return mysql_affected_rows($this->connection); + } + } + + public function getLastId() { + if ($this->connection) { + return mysql_insert_id($this->connection); + } + } + + public function isConnected() { + if ($this->connection) { + return true; + } else { + return false; + } + } + + public function __destruct() { + if ($this->connection) { + mysql_close($this->connection); + } + } +}
\ No newline at end of file diff --git a/public/system/library/db/mysqli.php b/public/system/library/db/mysqli.php new file mode 100644 index 0000000..2e40109 --- /dev/null +++ b/public/system/library/db/mysqli.php @@ -0,0 +1,63 @@ +<?php +namespace DB; +final class MySQLi { + private $connection; + + public function __construct($hostname, $username, $password, $database, $port = '3306') { + $this->connection = new \mysqli($hostname, $username, $password, $database, $port); + + if ($this->connection->connect_error) { + throw new \Exception('Error: ' . $this->connection->error . '<br />Error No: ' . $this->connection->errno); + } + + $this->connection->set_charset("utf8"); + $this->connection->query("SET SQL_MODE = ''"); + } + + public function query($sql) { + $query = $this->connection->query($sql); + + if (!$this->connection->errno) { + if ($query instanceof \mysqli_result) { + $data = array(); + + while ($row = $query->fetch_assoc()) { + $data[] = $row; + } + + $result = new \stdClass(); + $result->num_rows = $query->num_rows; + $result->row = isset($data[0]) ? $data[0] : array(); + $result->rows = $data; + + $query->close(); + + return $result; + } else { + return true; + } + } else { + throw new \Exception('Error: ' . $this->connection->error . '<br />Error No: ' . $this->connection->errno . '<br />' . $sql); + } + } + + public function escape($value) { + return $this->connection->real_escape_string($value); + } + + public function countAffected() { + return $this->connection->affected_rows; + } + + public function getLastId() { + return $this->connection->insert_id; + } + + public function connected() { + return $this->connection->ping(); + } + + public function __destruct() { + $this->connection->close(); + } +} diff --git a/public/system/library/db/postgre.php b/public/system/library/db/postgre.php new file mode 100644 index 0000000..9d86b1e --- /dev/null +++ b/public/system/library/db/postgre.php @@ -0,0 +1,68 @@ +<?php +namespace DB; +final class Postgre { + private $link; + + public function __construct($hostname, $username, $password, $database, $port = '5432') { + if (!$this->link = pg_connect('hostname=' . $hostname . ' port=' . $port . ' username=' . $username . ' password=' . $password . ' database=' . $database)) { + throw new \Exception('Error: Could not make a database link using ' . $username . '@' . $hostname); + } + + if (!mysql_select_db($database, $this->link)) { + throw new \Exception('Error: Could not connect to database ' . $database); + } + + pg_query($this->link, "SET CLIENT_ENCODING TO 'UTF8'"); + } + + public function query($sql) { + $resource = pg_query($this->link, $sql); + + if ($resource) { + if (is_resource($resource)) { + $i = 0; + + $data = array(); + + while ($result = pg_fetch_assoc($resource)) { + $data[$i] = $result; + + $i++; + } + + pg_free_result($resource); + + $query = new \stdClass(); + $query->row = isset($data[0]) ? $data[0] : array(); + $query->rows = $data; + $query->num_rows = $i; + + unset($data); + + return $query; + } else { + return true; + } + } else { + throw new \Exception('Error: ' . pg_result_error($this->link) . '<br />' . $sql); + } + } + + public function escape($value) { + return pg_escape_string($this->link, $value); + } + + public function countAffected() { + return pg_affected_rows($this->link); + } + + public function getLastId() { + $query = $this->query("SELECT LASTVAL() AS `id`"); + + return $query->row['id']; + } + + public function __destruct() { + pg_close($this->link); + } +}
\ No newline at end of file diff --git a/public/system/library/document.php b/public/system/library/document.php new file mode 100644 index 0000000..ffdc4ad --- /dev/null +++ b/public/system/library/document.php @@ -0,0 +1,147 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Document class +*/ +class Document { + private $title; + private $description; + private $keywords; + private $links = array(); + private $styles = array(); + private $scripts = array(); + + /** + * + * + * @param string $title + */ + public function setTitle($title) { + $this->title = $title; + } + + /** + * + * + * @return string + */ + public function getTitle() { + return $this->title; + } + + /** + * + * + * @param string $description + */ + public function setDescription($description) { + $this->description = $description; + } + + /** + * + * + * @param string $description + * + * @return string + */ + public function getDescription() { + return $this->description; + } + + /** + * + * + * @param string $keywords + */ + public function setKeywords($keywords) { + $this->keywords = $keywords; + } + + /** + * + * + * @return string + */ + public function getKeywords() { + return $this->keywords; + } + + /** + * + * + * @param string $href + * @param string $rel + */ + public function addLink($href, $rel) { + $this->links[$href] = array( + 'href' => $href, + 'rel' => $rel + ); + } + + /** + * + * + * @return array + */ + public function getLinks() { + return $this->links; + } + + /** + * + * + * @param string $href + * @param string $rel + * @param string $media + */ + public function addStyle($href, $rel = 'stylesheet', $media = 'screen') { + $this->styles[$href] = array( + 'href' => $href, + 'rel' => $rel, + 'media' => $media + ); + } + + /** + * + * + * @return array + */ + public function getStyles() { + return $this->styles; + } + + /** + * + * + * @param string $href + * @param string $postion + */ + public function addScript($href, $postion = 'header') { + $this->scripts[$postion][$href] = $href; + } + + /** + * + * + * @param string $postion + * + * @return array + */ + public function getScripts($postion = 'header') { + if (isset($this->scripts[$postion])) { + return $this->scripts[$postion]; + } else { + return array(); + } + } +}
\ No newline at end of file diff --git a/public/system/library/encryption.php b/public/system/library/encryption.php new file mode 100644 index 0000000..66e863c --- /dev/null +++ b/public/system/library/encryption.php @@ -0,0 +1,37 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Encryption class +*/ +final class Encryption { + /** + * + * + * @param string $key + * @param string $value + * + * @return string + */ + public function encrypt($key, $value) { + return strtr(base64_encode(openssl_encrypt($value, 'aes-128-cbc', hash('sha256', $key, true))), '+/=', '-_,'); + } + + /** + * + * + * @param string $key + * @param string $value + * + * @return string + */ + public function decrypt($key, $value) { + return trim(openssl_decrypt(base64_decode(strtr($value, '-_,', '+/=')), 'aes-128-cbc', hash('sha256', $key, true))); + } +}
\ No newline at end of file diff --git a/public/system/library/googleshopping/cron.php b/public/system/library/googleshopping/cron.php new file mode 100644 index 0000000..823d832 --- /dev/null +++ b/public/system/library/googleshopping/cron.php @@ -0,0 +1,9 @@ +<?php + +$current_dir = dirname(__FILE__); + +require_once $current_dir . DIRECTORY_SEPARATOR . 'cron_functions.php'; + +if ($index = advertise_google_init($current_dir)) { + require_once $index; +}
\ No newline at end of file diff --git a/public/system/library/googleshopping/cron_functions.php b/public/system/library/googleshopping/cron_functions.php new file mode 100644 index 0000000..c24bfc3 --- /dev/null +++ b/public/system/library/googleshopping/cron_functions.php @@ -0,0 +1,42 @@ +<?php + +function advertise_google_validate() { + if (!getenv("ADVERTISE_GOOGLE_CRON")) { + die("Not in Command Line." . PHP_EOL); + } +} + +function advertise_google_chdir($current_dir) { + $root_dir = dirname(dirname(dirname($current_dir))); + + chdir($root_dir); + + return $root_dir; +} + +function advertise_google_define_route() { + define('ADVERTISE_GOOGLE_ROUTE', 'extension/advertise/google/cron'); + + $_GET['route'] = ADVERTISE_GOOGLE_ROUTE; +} + +function advertise_google_init($current_dir) { + // Validate environment + advertise_google_validate(); + + // Set up default server vars + $_SERVER["HTTP_HOST"] = getenv("CUSTOM_SERVER_NAME"); + $_SERVER["SERVER_NAME"] = getenv("CUSTOM_SERVER_NAME"); + $_SERVER["SERVER_PORT"] = getenv("CUSTOM_SERVER_PORT"); + + putenv("SERVER_NAME=" . $_SERVER["SERVER_NAME"]); + + // Change root dir + $root_dir = advertise_google_chdir($current_dir); + + advertise_google_define_route(); + + if (file_exists($root_dir . '/index.php')) { + return $root_dir . '/index.php'; + } +}
\ No newline at end of file diff --git a/public/system/library/googleshopping/exception/accessforbidden.php b/public/system/library/googleshopping/exception/accessforbidden.php new file mode 100644 index 0000000..9ed65e6 --- /dev/null +++ b/public/system/library/googleshopping/exception/accessforbidden.php @@ -0,0 +1,6 @@ +<?php + +namespace googleshopping\Exception; + +class AccessForbidden extends \RuntimeException { +} diff --git a/public/system/library/googleshopping/exception/connection.php b/public/system/library/googleshopping/exception/connection.php new file mode 100644 index 0000000..fa414b6 --- /dev/null +++ b/public/system/library/googleshopping/exception/connection.php @@ -0,0 +1,6 @@ +<?php + +namespace googleshopping\Exception; + +class Connection extends \RuntimeException { +} diff --git a/public/system/library/googleshopping/googleshopping.php b/public/system/library/googleshopping/googleshopping.php new file mode 100644 index 0000000..afe3d5c --- /dev/null +++ b/public/system/library/googleshopping/googleshopping.php @@ -0,0 +1,1919 @@ +<?php + +namespace googleshopping; + +use \googleshopping\traits\StoreLoader; +use \googleshopping\exception\Connection as ConnectionException; +use \googleshopping\exception\AccessForbidden as AccessForbiddenException; + +class Googleshopping extends Library { + use StoreLoader; + + const API_URL = 'https://campaigns.opencart.com/'; + const CACHE_CAMPAIGN_REPORT = 21600; // In seconds + const CACHE_PRODUCT_REPORT = 21600; // In seconds + const ROAS_WAIT_INTERVAL = 1209600; // In seconds + const MICROAMOUNT = 1000000; + const DEBUG_LOG_FILENAME = 'googleshopping.%s.log'; + const ENDPOINT_ACCESS_TOKEN = 'api/access_token'; + const ENDPOINT_ACCESS_TOKEN_TEST = 'api/access_token/test'; + const ENDPOINT_CAMPAIGN_DELETE = 'api/campaign/delete'; + const ENDPOINT_CAMPAIGN_STATUS = 'api/campaign/status'; + const ENDPOINT_CAMPAIGN_TEST = 'api/campaign/test'; + const ENDPOINT_CAMPAIGN_UPDATE = 'api/campaign/update'; + const ENDPOINT_CONVERSION_TRACKER = 'api/conversion_tracker'; + const ENDPOINT_DATAFEED_CLOSE = 'api/datafeed/close'; + const ENDPOINT_DATAFEED_INIT = 'api/datafeed/init'; + const ENDPOINT_DATAFEED_PUSH = 'api/datafeed/push'; + const ENDPOINT_MERCHANT_AUTH_URL = 'api/merchant/authorize_url'; + const ENDPOINT_MERCHANT_AVAILABLE_CARRIERS = 'api/merchant/available_carriers'; + const ENDPOINT_MERCHANT_DISCONNECT = 'api/merchant/disconnect'; + const ENDPOINT_MERCHANT_PRODUCT_STATUSES = 'api/merchant/product_statuses'; + const ENDPOINT_MERCHANT_SHIPPING_TAXES = 'api/merchant/shipping_taxes'; + const ENDPOINT_REPORT_AD = 'api/report/ad&interval=%s'; + const ENDPOINT_REPORT_CAMPAIGN = 'api/report/campaign&interval=%s'; + const ENDPOINT_VERIFY_IS_CLAIMED = 'api/verify/is_claimed'; + const ENDPOINT_VERIFY_SITE = 'api/verify/site'; + const ENDPOINT_VERIFY_TOKEN = 'api/verify/token'; + const SCOPES = 'OC_FEED REPORT ADVERTISE'; + + private $event_snippet; + private $purchase_data; + private $store_url; + private $store_name; + private $endpoint_url; + private $store_id = 0; + private $debug_log; + + public function __construct($registry, $store_id) { + parent::__construct($registry); + + $this->store_id = $store_id; + + $this->load->model('setting/setting'); + + if ($this->store_id === 0) { + $this->store_url = basename(DIR_TEMPLATE) == 'template' ? HTTPS_CATALOG : HTTPS_SERVER; + $this->store_name = $this->config->get('config_name'); + } else { + $this->store_url = $this->model_setting_setting->getSettingValue('config_ssl', $store_id); + $this->store_name = $this->model_setting_setting->getSettingValue('config_name', $store_id); + } + + $this->endpoint_url = self::API_URL . 'index.php?route=%s'; + + $this->loadStore($this->store_id); + + $this->debug_log = new Log(sprintf(self::DEBUG_LOG_FILENAME, $this->store_id)); + } + + public function getStoreUrl() { + return $this->store_url; + } + + public function getStoreName() { + return $this->store_name; + } + + public function getSupportedLanguageId($code) { + $this->load->model('localisation/language'); + + foreach ($this->model_localisation_language->getLanguages() as $language) { + $language_code = current(explode("-", $language['code'])); + + if ($this->compareTrimmedLowercase($code, $language_code) === 0) { + return (int)$language['language_id']; + } + } + + return 0; + } + + public function getSupportedCurrencyId($code) { + $this->load->model('localisation/currency'); + + foreach ($this->model_localisation_currency->getCurrencies() as $currency) { + if ($this->compareTrimmedLowercase($code, $currency['code']) === 0) { + return (int)$currency['currency_id']; + } + } + + return 0; + } + + public function getCountryName($code) { + $this->load->config('googleshopping/googleshopping'); + + $this->load->model('localisation/country'); + + $countries = $this->config->get('advertise_google_countries'); + + // Default value + $result = $countries[$code]; + + // Override with store value, if present + foreach ($this->model_localisation_country->getCountries() as $store_country) { + if ($this->compareTrimmedLowercase($store_country['iso_code_2'], $code) === 0) { + $result = $store_country['name']; + break; + } + } + + return $result; + } + + public function compareTrimmedLowercase($text1, $text2) { + return strcmp(strtolower(trim($text1)), strtolower(trim($text2))); + } + + public function getTargets($store_id) { + $sql = "SELECT * FROM `" . DB_PREFIX . "googleshopping_target` WHERE store_id=" . $store_id; + + return array_map(array($this, 'target'), $this->db->query($sql)->rows); + } + + public function getTarget($advertise_google_target_id) { + $sql = "SELECT * FROM `" . DB_PREFIX . "googleshopping_target` WHERE advertise_google_target_id=" . (int)$advertise_google_target_id; + + return $this->target($this->db->query($sql)->row); + } + + public function editTarget($target_id, $target) { + $sql = "UPDATE `" . DB_PREFIX . "googleshopping_target` SET `campaign_name`='" . $this->db->escape($target['campaign_name']) . "', `country`='" . $this->db->escape($target['country']) . "', `budget`='" . (float)$target['budget'] . "', `feeds`='" . $this->db->escape(json_encode($target['feeds'])) . "', `roas`='" . (int)$target['roas'] . "', `status`='" . $this->db->escape($target['status']) . "' WHERE `advertise_google_target_id`='" . (int)$target_id . "'"; + + $this->db->query($sql); + + return $target; + } + + public function deleteTarget($target_id) { + $sql = "DELETE FROM `" . DB_PREFIX . "googleshopping_target` WHERE `advertise_google_target_id`='" . (int)$target_id . "'"; + + $this->db->query($sql); + + $sql = "DELETE FROM `" . DB_PREFIX . "googleshopping_product_target` WHERE `advertise_google_target_id`='" . (int)$target_id . "'"; + + $this->db->query($sql); + + return true; + } + + public function doJob($job) { + $product_count = 0; + + // Initialize push + $init_request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_DATAFEED_INIT, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => array( + 'work_id' => $job['work_id'] + ) + ); + + $response = $this->api($init_request); + + // At this point, the job has been initialized and we can start pushing the datafeed + $page = 0; + + while (null !== $products = $this->getFeedProducts(++$page, $job['language_id'], $job['currency'])) { + $post = array(); + + $post_data = array( + 'product' => $products, + 'work_id' => $job['work_id'], + 'work_step' => $response['work_step'] + ); + + $this->curlPostQuery($post_data, $post); + + $push_request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_DATAFEED_PUSH, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => $post + ); + + $response = $this->api($push_request); + + $product_count += count($products); + } + + // Finally, close the file to finish the job + $close_request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_DATAFEED_CLOSE, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => array( + 'work_id' => $job['work_id'], + 'work_step' => $response['work_step'] + ) + ); + + $this->api($close_request); + + return $product_count; + } + + public function getProductVariationIds($page) { + $this->load->config('googleshopping/googleshopping'); + + $sql = "SELECT DISTINCT pag.product_id, pag.color, pag.size FROM `" . DB_PREFIX . "googleshopping_product` pag LEFT JOIN `" . DB_PREFIX . "product` p ON (p.product_id = pag.product_id) LEFT JOIN `" . DB_PREFIX . "product_to_store` p2s ON (p2s.product_id = p.product_id AND p2s.store_id=" . (int)$this->store_id . ") WHERE p2s.store_id IS NOT NULL AND p.status = 1 AND p.date_available <= NOW() AND p.price > 0 ORDER BY p.product_id ASC LIMIT " . (int)(($page - 1) * $this->config->get('advertise_google_report_limit')) . ', ' . (int)$this->config->get('advertise_google_report_limit'); + + $result = array(); + + $this->load->model('localisation/language'); + + foreach ($this->db->query($sql)->rows as $row) { + foreach ($this->model_localisation_language->getLanguages() as $language) { + $groups = $this->getGroups($row['product_id'], $language['language_id'], $row['color'], $row['size']); + + foreach (array_keys($groups) as $id) { + if (!in_array($id, $result)) { + $result[] = $id; + } + } + } + } + + return !empty($result) ? $result : null; + } + + // A copy of the OpenCart SEO URL rewrite method. + 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 . "seo_url WHERE `query` = '" . $this->db->escape($key . '=' . (int)$value) . "' AND store_id = '" . (int)$this->config->get('config_store_id') . "' AND language_id = '" . (int)$this->config->get('config_language_id') . "'"); + + if ($query->num_rows && $query->row['keyword']) { + $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 . "seo_url WHERE `query` = 'category_id=" . (int)$category . "' AND store_id = '" . (int)$this->config->get('config_store_id') . "' AND language_id = '" . (int)$this->config->get('config_language_id') . "'"); + + if ($query->num_rows && $query->row['keyword']) { + $url .= '/' . $query->row['keyword']; + } else { + $url = ''; + + break; + } + } + + unset($data[$key]); + } + } + } + + if ($url) { + unset($data['route']); + + $query = ''; + + if ($data) { + foreach ($data as $key => $value) { + $query .= '&' . rawurlencode((string)$key) . '=' . rawurlencode((is_array($value) ? http_build_query($value) : (string)$value)); + } + + if ($query) { + $query = '?' . str_replace('&', '&', 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; + } + } + + protected function convertedTaxedPrice($value, $tax_class_id, $currency) { + return number_format($this->currency->convert($this->tax->calculate($value, $tax_class_id, $this->config->get('config_tax')), $this->config->get('config_currency'), $currency), 2, '.', ''); + } + + protected function getFeedProducts($page, $language_id, $currency) { + $sql = $this->getFeedProductsQuery($page, $language_id); + + $result = array(); + + $this->setRuntimeExceptionErrorHandler(); + + foreach ($this->db->query($sql)->rows as $row) { + try { + if (!empty($row['image']) && is_file(DIR_IMAGE . $row['image']) && is_readable(DIR_IMAGE . $row['image'])) { + $image = $this->resize($row['image'], 250, 250); + } else { + throw new \RuntimeException("Image does not exist or cannot be read."); + } + } catch (\RuntimeException $e) { + $this->output(sprintf("Error for product %s: %s", $row['model'], $e->getMessage())); + + $image = $this->resize('no_image.png', 250, 250); + } + + $url = new \Url($this->store_url, $this->store_url); + + if ($this->config->get('config_seo_url')) { + $url->addRewrite($this); + } + + $price = $this->convertedTaxedPrice($row['price'], $row['tax_class_id'], $currency); + + $special_price = null; + + if ($row['special_price'] !== null) { + $parts = explode('<[S]>', $row['special_price']); + + $special_price = array( + 'value' => $this->convertedTaxedPrice($parts[0], $row['tax_class_id'], $currency), + 'currency' => $currency + ); + + if ($parts[1] >= '1970-01-01') { + $special_price['start'] = $parts[1]; + } + + if ($parts[2] >= '1970-01-01') { + $special_price['end'] = $parts[2]; + } + } + + $campaigns = array(); + $custom_label_0 = ''; + $custom_label_1 = ''; + $custom_label_2 = ''; + $custom_label_3 = ''; + $custom_label_4 = ''; + + if (!empty($row['campaign_names'])) { + $campaigns = explode('<[S]>', $row['campaign_names']); + $i = 0; + + do { + ${'custom_label_' . ($i++)} = trim(strtolower(array_pop($campaigns))); + } while (!empty($campaigns)); + } + + $mpn = !empty($row['mpn']) ? $row['mpn'] : ''; + + if (!empty($row['upc'])) { + $gtin = $row['upc']; + } else if (!empty($row['ean'])) { + $gtin = $row['ean']; + } else if (!empty($row['jan'])) { + $gtin = $row['jan']; + } else if (!empty($row['isbn'])) { + $gtin = $row['isbn']; + } else { + $gtin = ''; + } + + $base_row = array( + 'adult' => !empty($row['adult']) ? 'yes' : 'no', + 'age_group' => !empty($row['age_group']) ? $row['age_group'] : '', + 'availability' => (int)$row['quantity'] > 0 && !$this->config->get('config_maintenance') ? 'in stock' : 'out of stock', + 'brand' => $this->sanitizeText($row['brand'], 70), + 'color' => '', + 'condition' => !empty($row['condition']) ? $row['condition'] : '', + 'custom_label_0' => $this->sanitizeText($custom_label_0, 100), + 'custom_label_1' => $this->sanitizeText($custom_label_1, 100), + 'custom_label_2' => $this->sanitizeText($custom_label_2, 100), + 'custom_label_3' => $this->sanitizeText($custom_label_3, 100), + 'custom_label_4' => $this->sanitizeText($custom_label_4, 100), + 'description' => $this->sanitizeText($row['description'], 5000), + 'gender' => !empty($row['gender']) ? $row['gender'] : '', + 'google_product_category' => !empty($row['google_product_category']) ? $row['google_product_category'] : '', + 'id' => $this->sanitizeText($row['product_id'], 50), + 'identifier_exists' => !empty($row['brand']) && !empty($mpn) ? 'yes' : 'no', + 'image_link' => $this->sanitizeText($image, 2000), + 'is_bundle' => !empty($row['is_bundle']) ? 'yes' : 'no', + 'item_group_id' => $this->sanitizeText($row['product_id'], 50), + 'link' => $this->sanitizeText(html_entity_decode($url->link('product/product', 'product_id=' . $row['product_id'], true), ENT_QUOTES, 'UTF-8'), 2000), + 'mpn' => $this->sanitizeText($mpn, 70), + 'gtin' => $this->sanitizeText($gtin, 14), + 'multipack' => !empty($row['multipack']) && (int)$row['multipack'] >= 2 ? (int)$row['multipack'] : '', // Cannot be 1!!! + 'price' => array( + 'value' => $price, + 'currency' => $currency + ), + 'size' => '', + 'size_system' => !empty($row['size_system']) ? $row['size_system'] : '', + 'size_type' => !empty($row['size_type']) ? $row['size_type'] : '', + 'title' => $this->sanitizeText($row['name'], 150) + ); + + // Provide optional special price + if ($special_price !== null) { + $base_row['special_price'] = $special_price; + } + + $groups = $this->getGroups($row['product_id'], $language_id, $row['color'], $row['size']); + + foreach ($groups as $id => $group) { + $base_row['id'] = $id; + $base_row['color'] = $this->sanitizeText($group['color'], 40); + $base_row['size'] = $this->sanitizeText($group['size'], 100); + + $result[] = $base_row; + } + } + + $this->restoreErrorHandler(); + + return !empty($result) ? $result : null; + } + + public function getGroups($product_id, $language_id, $color_id, $size_id) { + $options = array( + 'color' => $this->getProductOptionValueNames($product_id, $language_id, $color_id), + 'size' => $this->getProductOptionValueNames($product_id, $language_id, $size_id) + ); + + $result = array(); + + foreach ($this->combineOptions($options) as $group) { + $key = $product_id . '-' . md5(json_encode(array('color' => $group['color'], 'size' => $group['size']))); + + $result[$key] = $group; + } + + return $result; + } + + public function getProductOptionValueNames($product_id, $language_id, $option_id) { + $sql = "SELECT DISTINCT pov.product_option_value_id, ovd.name FROM `" . DB_PREFIX . "product_option_value` pov LEFT JOIN `" . DB_PREFIX . "option_value_description` ovd ON (ovd.option_value_id = pov.option_value_id) WHERE pov.product_id=" . (int)$product_id . " AND pov.option_id=" . (int)$option_id . " AND ovd.language_id=" . (int)$language_id; + + $result = $this->db->query($sql); + + if ($result->num_rows > 0) { + $return = array(); + + foreach ($result->rows as $row) { + $text = $this->sanitizeText($row['name'], 100); + $name = implode('/', array_slice(array_filter(array_map('trim', preg_split('~[,/;]+~i', $text))), 0, 3)); + + $return[$row['product_option_value_id']] = $name; + } + + return $return; + } + + return array(''); + } + + public function applyFilter(&$sql, &$data) { + if (!empty($data['filter_product_name'])) { + $sql .= " AND pd.name LIKE '" . $this->db->escape($data['filter_product_name']) . "%'"; + } + + if (!empty($data['filter_product_model'])) { + $sql .= " AND p.model LIKE '" . $this->db->escape($data['filter_product_model']) . "%'"; + } + + if (!empty($data['filter_category_id'])) { + $sql .= " AND p.product_id IN (SELECT p2c_t.product_id FROM `" . DB_PREFIX . "category_path` cp_t LEFT JOIN `" . DB_PREFIX . "product_to_category` p2c_t ON (p2c_t.category_id=cp_t.category_id) WHERE cp_t.path_id=" . (int)$data['filter_category_id'] . ")"; + } + + if (isset($data['filter_is_modified']) && $data['filter_is_modified'] !== "") { + $sql .= " AND p.product_id IN (SELECT pag_t.product_id FROM `" . DB_PREFIX . "googleshopping_product` pag_t WHERE pag_t.is_modified=" . (int)$data['filter_is_modified'] . ")"; + } + + if (!empty($data['filter_store_id'])) { + $sql .= " AND p.product_id IN (SELECT p2s_t.product_id FROM `" . DB_PREFIX . "product_to_store` p2s_t WHERE p2s_t.store_id=" . (int)$data['filter_store_id'] . ")"; + } + } + + public function getProducts($data, $store_id) { + $sql = "SELECT pag.*, p.product_id, p.image, pd.name, p.model FROM `" . DB_PREFIX . "product` p LEFT JOIN `" . DB_PREFIX . "product_description` pd ON (p.product_id = pd.product_id) LEFT JOIN `" . DB_PREFIX . "googleshopping_product` pag ON (pag.product_id = p.product_id AND pag.store_id = " . (int)$store_id . ") WHERE pag.store_id IS NOT NULL AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "'"; + + $this->applyFilter($sql, $data); + + $sql .= " GROUP BY p.product_id"; + + $sort_data = array( + 'name', + 'model', + 'impressions', + 'clicks', + 'cost', + 'conversions', + 'conversion_value', + 'has_issues', + 'destination_status' + ); + + if (isset($data['sort']) && in_array($data['sort'], $sort_data)) { + $sql .= " ORDER BY " . $data['sort']; + } else { + $sql .= " ORDER BY name"; + } + + if (isset($data['order']) && ($data['order'] == 'DESC')) { + $sql .= " DESC"; + } else { + $sql .= " ASC"; + } + + if (isset($data['start']) || isset($data['limit'])) { + if ($data['start'] < 0) { + $data['start'] = 0; + } + + if ($data['limit'] < 1) { + $data['limit'] = 20; + } + + $sql .= " LIMIT " . (int)$data['start'] . "," . (int)$data['limit']; + } + + return $this->db->query($sql)->rows; + } + + public function getTotalProducts($data, $store_id) { + $sql = "SELECT COUNT(*) as total FROM `" . DB_PREFIX . "product` p LEFT JOIN `" . DB_PREFIX . "product_description` pd ON (p.product_id = pd.product_id) LEFT JOIN `" . DB_PREFIX . "googleshopping_product` pag ON (pag.product_id = p.product_id AND pag.store_id = " . (int)$store_id . ") WHERE pag.store_id IS NOT NULL AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "'"; + + $this->applyFilter($sql, $data); + + return (int)$this->db->query($sql)->row['total']; + } + + public function getProductIds($data, $store_id) { + $result = array(); + + $this->load->model('localisation/language'); + + foreach ($this->getProducts($data, $store_id) as $row) { + $product_id = (int)$row['product_id']; + + if (!in_array($product_id, $result)) { + $result[] = $product_id; + } + } + + return $result; + } + + public function clearProductStatuses($product_ids, $store_id) { + $sql = "UPDATE `" . DB_PREFIX . "googleshopping_product_status` SET `destination_statuses`='', `data_quality_issues`='', `item_level_issues`='', `google_expiration_date`=0 WHERE `product_id` IN (" . $this->productIdsToIntegerExpression($product_ids) . ") AND `store_id`=" . (int)$store_id; + + $this->db->query($sql); + + $sql = "UPDATE `" . DB_PREFIX . "googleshopping_product` SET `has_issues`=0, `destination_status`='pending' WHERE `product_id` IN (" . $this->productIdsToIntegerExpression($product_ids) . ") AND `store_id`=" . (int)$store_id; + + $this->db->query($sql); + } + + public function productIdsToIntegerExpression($product_ids) { + return implode(",", array_map(array($this, 'integer'), $product_ids)); + } + + public function integer(&$product_id) { + if (!is_numeric($product_id)) { + return 0; + } else { + return (int)$product_id; + } + } + + public function cron() { + $this->enableErrorReporting(); + + $this->load->config('googleshopping/googleshopping'); + + $report = array(); + + $report[] = $this->output("Starting CRON task for " . $this->getStoreUrl()); + + try { + $report[] = $this->output("Refreshing access token."); + + $this->isConnected(); + } catch (\RuntimeException $e) { + $report[] = $this->output($e->getMessage()); + } + + $default_config_tax = $this->config->get("config_tax"); + $default_config_store_id = $this->config->get("config_store_id"); + $default_config_language_id = $this->config->get("config_language_id"); + $default_config_seo_url = $this->config->get("config_seo_url"); + + // Do product feed uploads + foreach ($this->getJobs() as $job) { + try { + $report[] = $this->output("Uploading product feed. Work ID: " . $job['work_id']); + + // Set the tax context for the job + if (in_array("US", $job['countries'])) { + // In case the feed is for the US, disable taxes because they are already configured on the merchant level by the extension + $this->config->set("config_tax", 0); + } + + // Set the store and language context for the job + $this->config->set("config_store_id", $this->store_id); + $this->config->set("config_language_id", $job['language_id']); + $this->config->set("config_seo_url", $this->model_setting_setting->getSettingValue("config_seo_url", $this->store_id)); + + // Do the CRON job + $count = $this->doJob($job); + + // Reset the taxes, store, and language to their original state + $this->config->set("config_tax", $default_config_tax); + $this->config->set("config_store_id", $default_config_store_id); + $this->config->set("config_language_id", $default_config_language_id); + $this->config->set("config_seo_url", $default_config_seo_url); + + $report[] = $this->output("Uploaded count: " . $count); + } catch (\RuntimeException $e) { + $report[] = $this->output($e->getMessage()); + } + } + + // Reset the taxes, store, and language to their original state + $this->config->set("config_tax", $default_config_tax); + $this->config->set("config_store_id", $default_config_store_id); + $this->config->set("config_language_id", $default_config_language_id); + $this->config->set("config_seo_url", $default_config_seo_url); + + // Pull product reports + $report[] = $this->output("Fetching product reports."); + + try { + $report_count = 0; + + $page = 0; + + $this->clearReports(); + + while (null !== $product_variation_ids = $this->getProductVariationIds(++$page)) { + foreach (array_chunk($product_variation_ids, (int)$this->config->get('advertise_google_report_limit')) as $chunk) { + $product_reports = $this->getProductReports($chunk); + + if (!empty($product_reports)) { + $this->updateProductReports($product_reports, $this->store_id); + $report_count += count($product_reports); + } + } + } + } catch (\RuntimeException $e) { + $report[] = $this->output($e->getMessage()); + } + + $report[] = $this->output("Fetched report count: " . $report_count); + + // Pull product statuses + $report[] = $this->output("Fetching product statuses."); + + $page = 1; + $status_count = 0; + + do { + $filter_data = array( + 'start' => ($page - 1) * $this->config->get('advertise_google_product_status_limit'), + 'limit' => $this->config->get('advertise_google_product_status_limit') + ); + + $page++; + + $product_variation_target_specific_ids = $this->getProductVariationTargetSpecificIds($filter_data); + + try { + // Fetch latest statuses from the API + if (!empty($product_variation_target_specific_ids)) { + $product_ids = $this->getProductIds($filter_data, $this->store_id); + + $this->clearProductStatuses($product_ids, $this->store_id); + + foreach (array_chunk($product_variation_target_specific_ids, (int)$this->config->get('advertise_google_product_status_limit')) as $chunk) { + $product_statuses = $this->getProductStatuses($chunk); + + if (!empty($product_statuses)) { + $this->updateProductStatuses($product_statuses); + $status_count += count($product_statuses); + } + } + } + } catch (\RuntimeException $e) { + $report[] = $this->output($e->getMessage()); + } + } while (!empty($product_variation_target_specific_ids)); + + $report[] = $this->output("Fetched status count: " . $status_count); + + $report[] = $this->output("CRON finished!"); + + $this->applyNewSetting('advertise_google_cron_last_executed', time()); + + $this->sendEmailReport($report); + } + + public function getProductVariationTargetSpecificIds($data) { + $result = array(); + + $targets = $this->getTargets($this->store_id); + + foreach ($this->getProducts($data, $this->store_id) as $row) { + foreach ($targets as $target) { + foreach ($target['feeds'] as $feed) { + $language_code = $feed['language']; + + $language_id = $this->getSupportedLanguageId($language_code); + + $groups = $this->getGroups($row['product_id'], $language_id, $row['color'], $row['size']); + + foreach (array_keys($groups) as $id) { + $id_parts = array(); + $id_parts[] = 'online'; + $id_parts[] = $language_code; + $id_parts[] = $target['country']['code']; + $id_parts[] = $id; + + $result_id = implode(':', $id_parts); + + if (!in_array($result_id, $result)) { + $result[] = $result_id; + } + } + } + } + } + + return $result; + } + + public function updateProductReports($reports) { + $values = array(); + + foreach ($reports as $report) { + $entry = array(); + $entry['product_id'] = $this->getProductIdFromOfferId($report['offer_id']); + $entry['store_id'] = (int)$this->store_id; + $entry['impressions'] = (int)$report['impressions']; + $entry['clicks'] = (int)$report['clicks']; + $entry['conversions'] = (int)$report['conversions']; + $entry['cost'] = ((int)$report['cost']) / self::MICROAMOUNT; + $entry['conversion_value'] = (float)$report['conversion_value']; + + $values[] = "(" . implode(",", $entry) . ")"; + } + + $sql = "INSERT INTO `" . DB_PREFIX . "googleshopping_product` (`product_id`, `store_id`, `impressions`, `clicks`, `conversions`, `cost`, `conversion_value`) VALUES " . implode(',', $values) . " ON DUPLICATE KEY UPDATE `impressions`=`impressions` + VALUES(`impressions`), `clicks`=`clicks` + VALUES(`clicks`), `conversions`=`conversions` + VALUES(`conversions`), `cost`=`cost` + VALUES(`cost`), `conversion_value`=`conversion_value` + VALUES(`conversion_value`)"; + + $this->db->query($sql); + } + + public function updateProductStatuses($statuses) { + $product_advertise_google = array(); + $product_advertise_google_status = array(); + $product_level_entries = array(); + $entry_statuses = array(); + + foreach ($statuses as $status) { + $product_id = $this->getProductIdFromTargetSpecificId($status['productId']); + $product_variation_id = $this->getProductVariationIdFromTargetSpecificId($status['productId']); + + if (!isset($product_level_entries[$product_id])) { + $product_level_entries[$product_id] = array( + 'product_id' => (int)$product_id, + 'store_id' => (int)$this->store_id, + 'has_issues' => 0, + 'destination_status' => 'pending' + ); + } + + foreach ($status['destinationStatuses'] as $destination_status) { + if (!$destination_status['approvalPending']) { + switch ($destination_status['approvalStatus']) { + case 'approved' : + if ($product_level_entries[$product_id]['destination_status'] == 'pending') { + $product_level_entries[$product_id]['destination_status'] = 'approved'; + } + break; + case 'disapproved' : + $product_level_entries[$product_id]['destination_status'] = 'disapproved'; + break; + } + } + } + + if (!$product_level_entries[$product_id]['has_issues']) { + if (!empty($status['dataQualityIssues']) || !empty($status['itemLevelIssues'])) { + $product_level_entries[$product_id]['has_issues'] = 1; + } + } + + if (!isset($entry_statuses[$product_variation_id])) { + $entry_statuses[$product_variation_id] = array(); + + $entry_statuses[$product_variation_id]['product_id'] = (int)$product_id; + $entry_statuses[$product_variation_id]['store_id'] = (int)$this->store_id; + $entry_statuses[$product_variation_id]['product_variation_id'] = "'" . $this->db->escape($product_variation_id) . "'"; + $entry_statuses[$product_variation_id]['destination_statuses'] = array(); + $entry_statuses[$product_variation_id]['data_quality_issues'] = array(); + $entry_statuses[$product_variation_id]['item_level_issues'] = array(); + $entry_statuses[$product_variation_id]['google_expiration_date'] = (int)strtotime($status['googleExpirationDate']); + } + + $entry_statuses[$product_variation_id]['destination_statuses'] = array_merge( + $entry_statuses[$product_variation_id]['destination_statuses'], + !empty($status['destinationStatuses']) ? $status['destinationStatuses'] : array() + ); + + $entry_statuses[$product_variation_id]['data_quality_issues'] = array_merge( + $entry_statuses[$product_variation_id]['data_quality_issues'], + !empty($status['dataQualityIssues']) ? $status['dataQualityIssues'] : array() + ); + + $entry_statuses[$product_variation_id]['item_level_issues'] = array_merge( + $entry_statuses[$product_variation_id]['item_level_issues'], + !empty($status['itemLevelIssues']) ? $status['itemLevelIssues'] : array() + ); + } + + foreach ($entry_statuses as &$entry_status) { + $entry_status['destination_statuses'] = "'" . $this->db->escape(json_encode($entry_status['destination_statuses'])) . "'"; + $entry_status['data_quality_issues'] = "'" . $this->db->escape(json_encode($entry_status['data_quality_issues'])) . "'"; + $entry_status['item_level_issues'] = "'" . $this->db->escape(json_encode($entry_status['item_level_issues'])) . "'"; + + $product_advertise_google_status[] = "(" . implode(",", $entry_status) . ")"; + } + + $sql = "INSERT INTO `" . DB_PREFIX . "googleshopping_product_status` (`product_id`, `store_id`, `product_variation_id`, `destination_statuses`, `data_quality_issues`, `item_level_issues`, `google_expiration_date`) VALUES " . implode(',', $product_advertise_google_status) . " ON DUPLICATE KEY UPDATE `destination_statuses`=VALUES(`destination_statuses`), `data_quality_issues`=VALUES(`data_quality_issues`), `item_level_issues`=VALUES(`item_level_issues`), `google_expiration_date`=VALUES(`google_expiration_date`)"; + + $this->db->query($sql); + + foreach ($product_level_entries as $entry) { + $entry['destination_status'] = "'" . $this->db->escape($entry['destination_status']) . "'"; + + $product_advertise_google[] = "(" . implode(",", $entry) . ")"; + } + + $sql = "INSERT INTO `" . DB_PREFIX . "googleshopping_product` (`product_id`, `store_id`, `has_issues`, `destination_status`) VALUES " . implode(',', $product_advertise_google) . " ON DUPLICATE KEY UPDATE `has_issues`=VALUES(`has_issues`), `destination_status`=VALUES(`destination_status`)"; + + $this->db->query($sql); + } + + protected function memoryLimitInBytes() { + $memory_limit = ini_get('memory_limit'); + + if (preg_match('/^(\d+)(.)$/', $memory_limit, $matches)) { + if ($matches[2] == 'G') { + $memory_limit = (int)$matches[1] * 1024 * 1024 * 1024; // nnnG -> nnn GB + } else if ($matches[2] == 'M') { + $memory_limit = (int)$matches[1] * 1024 * 1024; // nnnM -> nnn MB + } else if ($matches[2] == 'K') { + $memory_limit = (int)$matches[1] * 1024; // nnnK -> nnn KB + } + } + + return (int)$memory_limit; + } + + protected function enableErrorReporting() { + ini_set('display_errors', 1); + ini_set('display_startup_errors', 1); + error_reporting(E_ALL); + } + + protected function getProductIdFromTargetSpecificId($target_specific_id) { + return (int)preg_replace('/^online:[a-z]{2}:[A-Z]{2}:(\d+)-[a-f0-9]{32}$/', '$1', $target_specific_id); + } + + protected function getProductVariationIdFromTargetSpecificId($target_specific_id) { + return preg_replace('/^online:[a-z]{2}:[A-Z]{2}:(\d+-[a-f0-9]{32})$/', '$1', $target_specific_id); + } + + protected function getProductIdFromOfferId($offer_id) { + return (int)preg_replace('/^(\d+)-[a-f0-9]{32}$/', '$1', $offer_id); + } + + protected function clearReports() { + $sql = "UPDATE `" . DB_PREFIX . "googleshopping_product` SET `impressions`=0, `clicks`=0, `conversions`=0, `cost`=0.0000, `conversion_value`=0.0000 WHERE `store_id`=" . (int)$this->store_id; + + $this->db->query($sql); + } + + protected function getJobs() { + $jobs = array(); + + if ($this->setting->has('advertise_google_work') && is_array($this->setting->get('advertise_google_work'))) { + $this->load->model('extension/advertise/google'); + + foreach ($this->setting->get('advertise_google_work') as $work) { + $supported_language_id = $this->getSupportedLanguageId($work['language']); + $supported_currency_id = $this->getSupportedCurrencyId($work['currency']); + + if (!empty($supported_language_id) && !empty($supported_currency_id)) { + $currency_info = $this->getCurrency($supported_currency_id); + + $jobs[] = array( + 'work_id' => $work['work_id'], + 'countries' => isset($work['countries']) && is_array($work['countries']) ? $work['countries'] : array(), + 'language_id' => $supported_language_id, + 'currency' => $currency_info['code'] + ); + } + } + } + + return $jobs; + } + + protected function output($message) { + $log_message = date('Y-m-d H:i:s - ') . $message; + + if (defined('STDOUT')) { + fwrite(STDOUT, $log_message . PHP_EOL); + } else { + echo $log_message . '<br /><hr />'; + } + + return $log_message; + } + + protected function sendEmailReport(&$report) { + if (!$this->setting->get('advertise_google_cron_email_status')) { + return; //Do nothing + } + + $this->load->language('extension/advertise/google'); + + $subject = $this->language->get('text_cron_email_subject'); + $message = sprintf($this->language->get('text_cron_email_message'), implode('<br/>', $report)); + + $mail = new \Mail(); + + $mail->protocol = $this->config->get('config_mail_protocol'); + $mail->parameter = $this->config->get('config_mail_parameter'); + + $mail->smtp_hostname = $this->config->get('config_mail_smtp_hostname'); + $mail->smtp_username = $this->config->get('config_mail_smtp_username'); + $mail->smtp_password = html_entity_decode($this->config->get('config_mail_smtp_password'), ENT_QUOTES, "UTF-8"); + $mail->smtp_port = $this->config->get('config_mail_smtp_port'); + $mail->smtp_timeout = $this->config->get('config_mail_smtp_timeout'); + + $mail->setTo($this->setting->get('advertise_google_cron_email')); + $mail->setFrom($this->config->get('config_email')); + $mail->setSender($this->config->get('config_name')); + $mail->setSubject(html_entity_decode($subject, ENT_QUOTES, "UTF-8")); + $mail->setText(strip_tags($message)); + $mail->setHtml($message); + + $mail->send(); + } + + protected function getOptionValueName($row) { + $text = $this->sanitizeText($row['name'], 100); + + return implode('/', array_slice(array_filter(array_map('trim', preg_split('~[,/;]+~i', $text))), 0, 3)); + } + + protected function combineOptions($arrays) { + // Based on: https://gist.github.com/cecilemuller/4688876 + $result = array(array()); + + foreach ($arrays as $property => $property_values) { + $tmp = array(); + foreach ($result as $result_item) { + foreach ($property_values as $property_value) { + $tmp[] = array_merge($result_item, array($property => $property_value)); + } + } + $result = $tmp; + } + + return $result; + } + + protected function resize($filename, $width, $height) { + if (!is_file(DIR_IMAGE . $filename) || substr(str_replace('\\', '/', realpath(DIR_IMAGE . $filename)), 0, strlen(DIR_IMAGE)) != str_replace('\\', '/', DIR_IMAGE)) { + throw new \RuntimeException("Invalid image filename: " . DIR_IMAGE . $filename); + } + + $extension = pathinfo($filename, PATHINFO_EXTENSION); + + $image_old = $filename; + $image_new = 'cache/' . utf8_substr($filename, 0, utf8_strrpos($filename, '.')) . '-' . (int)$width . 'x' . (int)$height . '.' . $extension; + + if (!is_file(DIR_IMAGE . $image_new) || (filemtime(DIR_IMAGE . $image_old) > filemtime(DIR_IMAGE . $image_new))) { + list($width_orig, $height_orig, $image_type) = getimagesize(DIR_IMAGE . $image_old); + + if ($width_orig * $height_orig * 4 > $this->memoryLimitInBytes() * 0.4) { + throw new \RuntimeException("Image too large, skipping: " . $image_old); + } + + if (!in_array($image_type, array(IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF))) { + throw new \RuntimeException("Unexpected image type, skipping: " . $image_old); + } + + $path = ''; + + $directories = explode('/', dirname($image_new)); + + foreach ($directories as $directory) { + $path = $path . '/' . $directory; + + if (!is_dir(DIR_IMAGE . $path)) { + @mkdir(DIR_IMAGE . $path, 0777); + } + } + + if ($width_orig != $width || $height_orig != $height) { + $image = new \Image(DIR_IMAGE . $image_old); + $image->resize($width, $height); + $image->save(DIR_IMAGE . $image_new); + } else { + copy(DIR_IMAGE . $image_old, DIR_IMAGE . $image_new); + } + } + + $image_new = str_replace(array(' ', ','), array('%20', '%2C'), $image_new); // fix bug when attach image on email (gmail.com). it is automatic changing space " " to + + + return $this->store_url . 'image/' . $image_new; + } + + protected function sanitizeText($text, $limit) { + return utf8_substr( + trim( + preg_replace( + '~\s+~', + ' ', + strip_tags( + html_entity_decode(htmlspecialchars_decode($text, ENT_QUOTES), ENT_QUOTES, 'UTF-8') + ) + ) + ), + 0, + $limit + ); + } + + protected function setRuntimeExceptionErrorHandler() { + set_error_handler(function($code, $message, $file, $line) { + if (error_reporting() === 0) { + return false; + } + + switch ($code) { + case E_NOTICE: + case E_USER_NOTICE: + $error = 'Notice'; + break; + case E_WARNING: + case E_USER_WARNING: + $error = 'Warning'; + break; + case E_ERROR: + case E_USER_ERROR: + $error = 'Fatal Error'; + break; + default: + $error = 'Unknown'; + break; + } + + $message = 'PHP ' . $error . ': ' . $message . ' in ' . $file . ' on line ' . $line; + + throw new \RuntimeException($message); + }); + } + + protected function restoreErrorHandler() { + restore_error_handler(); + } + + protected function getFeedProductsQuery($page, $language_id) { + $this->load->config('googleshopping/googleshopping'); + + $sql = "SELECT p.product_id, pd.name, pd.description, p.image, p.quantity, p.price, p.mpn, p.ean, p.jan, p.isbn, p.upc, p.model, p.tax_class_id, IFNULL((SELECT m.name FROM `" . DB_PREFIX . "manufacturer` m WHERE m.manufacturer_id = p.manufacturer_id), '') as brand, (SELECT GROUP_CONCAT(agt.campaign_name SEPARATOR '<[S]>') FROM `" . DB_PREFIX . "googleshopping_product_target` pagt LEFT JOIN `" . DB_PREFIX . "googleshopping_target` agt ON (agt.advertise_google_target_id = pagt.advertise_google_target_id) WHERE pagt.product_id = p.product_id AND pagt.store_id = p2s.store_id GROUP BY pagt.product_id) as campaign_names, (SELECT CONCAT_WS('<[S]>', ps.price, ps.date_start, ps.date_end) FROM `" . DB_PREFIX . "product_special` ps WHERE ps.product_id=p.product_id AND ps.customer_group_id=" . (int)$this->config->get('config_customer_group_id') . " AND ((ps.date_start = '0000-00-00' OR ps.date_start < NOW()) AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW())) ORDER BY ps.priority ASC, ps.price ASC LIMIT 1) as special_price, pag.google_product_category, pag.condition, pag.adult, pag.multipack, pag.is_bundle, pag.age_group, pag.color, pag.gender, pag.size_type, pag.size_system, pag.size FROM `" . DB_PREFIX . "product` p LEFT JOIN `" . DB_PREFIX . "product_to_store` p2s ON (p2s.product_id = p.product_id AND p2s.store_id=" . (int)$this->store_id . ") LEFT JOIN `" . DB_PREFIX . "product_description` pd ON (pd.product_id = p.product_id) LEFT JOIN `" . DB_PREFIX . "googleshopping_product` pag ON (pag.product_id = p.product_id AND pag.store_id = p2s.store_id) WHERE p2s.store_id IS NOT NULL AND pd.language_id=" . (int)$language_id . " AND pd.name != '' AND pd.description != '' AND pd.name IS NOT NULL AND pd.description IS NOT NULL AND p.image != '' AND p.status = 1 AND p.date_available <= NOW() AND p.price > 0 ORDER BY p.product_id ASC LIMIT " . (int)(($page - 1) * $this->config->get('advertise_google_push_limit')) . ', ' . (int)$this->config->get('advertise_google_push_limit'); + + return $sql; + } + + public function setEventSnippet($snippet) { + $this->event_snippet = $snippet; + } + + public function getEventSnippet() { + return $this->event_snippet; + } + + public function getEventSnippetSendTo() { + $tracker = $this->setting->get('advertise_google_conversion_tracker'); + + if (!empty($tracker['google_event_snippet'])) { + $matches = array(); + + preg_match('~send_to\': \'([a-zA-Z0-9-]*).*\'~', $tracker['google_event_snippet'], $matches); + + return $matches[1]; + } + + return null; + } + + public function setPurchaseData($total) { + $this->purchase_data = $total; + } + + public function getPurchaseData() { + return $this->purchase_data; + } + + public function convertAndFormat($price, $currency) { + $currency_converter = new \Cart\Currency($this->registry); + $converted_price = $currency_converter->convert((float)$price, $this->config->get('config_currency'), $currency); + return (float)number_format($converted_price, 2, '.', ''); + } + + public function getMerchantAuthUrl($data) { + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_MERCHANT_AUTH_URL, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => $data + ); + + $response = $this->api($request); + + return $response['url']; + } + + public function isConnected() { + $settings_exist = + $this->setting->has('advertise_google_access_token') && + $this->setting->has('advertise_google_refresh_token') && + $this->setting->has('advertise_google_app_id') && + $this->setting->has('advertise_google_app_secret'); + + if ($settings_exist) { + if ($this->testAccessToken() || $this->getAccessToken()) { + return true; + } + } + + throw new ConnectionException("Access unavailable. Please re-connect."); + } + + public function isStoreUrlClaimed() { + // No need to check the connection here - this method is called immediately after checking it + + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_VERIFY_IS_CLAIMED, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => array( + 'url_website' => $this->store_url + ) + ); + + $response = $this->api($request); + + return $response['is_claimed']; + } + + public function currencyFormat($value) { + return '$' . number_format($value, 2, '.', ','); + } + + public function getCampaignReports() { + $targets = array(); + $statuses = array(); + + foreach ($this->getTargets($this->store_id) as $target) { + $targets[] = $target['campaign_name']; + $statuses[$target['campaign_name']] = $target['status']; + } + $targets[] = 'Total'; + + $cache = new \Cache($this->config->get('cache_engine'), self::CACHE_CAMPAIGN_REPORT); + $cache_key = 'advertise_google.' . $this->store_id . '.campaign_reports.' . md5(json_encode(array_keys($statuses)) . $this->setting->get('advertise_google_reporting_interval')); + + $cache_result = $cache->get($cache_key); + + if (empty($cache_result['result']) || (isset($cache_result['timestamp']) && $cache_result['timestamp'] >= time() + self::CACHE_CAMPAIGN_REPORT)) { + $request = array( + 'endpoint' => sprintf(self::ENDPOINT_REPORT_CAMPAIGN, $this->setting->get('advertise_google_reporting_interval')), + 'use_access_token' => true + ); + + $csv = $this->api($request); + + $lines = explode("\n", trim($csv['campaign_report'])); + + $result = array( + 'date_range' => null, + 'reports' => array() + ); + + // Get date range + $matches = array(); + preg_match('~CAMPAIGN_PERFORMANCE_REPORT \((.*?)\)~', $lines[0], $matches); + $result['date_range'] = $matches[1]; + + $header = explode(',', $lines[1]); + $data = array(); + $total = array(); + $value_keys = array(); + + $campaign_keys = array_flip($targets); + + $expected = array( + 'Campaign' => 'campaign_name', + 'Impressions' => 'impressions', + 'Clicks' => 'clicks', + 'Cost' => 'cost', + 'Conversions' => 'conversions', + 'Total conv. value' => 'conversion_value' + ); + + foreach ($header as $i => $title) { + if (!in_array($title, array_keys($expected))) { + continue; + } + + $value_keys[$i] = $expected[$title]; + } + + // Fill blank values + foreach ($campaign_keys as $campaign_name => $l) { + foreach ($value_keys as $i => $key) { + $result['reports'][$l][$key] = $key == 'campaign_name' ? $campaign_name : '–'; + } + } + + // Fill actual values + for ($j = 2; $j < count($lines); $j++) { + $line_items = explode(',', $lines[$j]); + $l = null; + + // Identify campaign key + foreach ($line_items as $k => $line_item_value) { + if (array_key_exists($k, $value_keys) && array_key_exists($line_item_value, $campaign_keys) && $value_keys[$k] == 'campaign_name') { + $l = $campaign_keys[$line_item_value]; + } + } + + // Fill campaign values + if (!is_null($l)) { + foreach ($line_items as $k => $line_item_value) { + if (!array_key_exists($k, $value_keys)) { + continue; + } + + if (in_array($value_keys[$k], array('cost'))) { + $line_item_value = $this->currencyFormat((float)$line_item_value / self::MICROAMOUNT); + } else if (in_array($value_keys[$k], array('conversion_value'))) { + $line_item_value = $this->currencyFormat((float)$line_item_value); + } else if ($value_keys[$k] == 'conversions') { + $line_item_value = (int)$line_item_value; + } + + $result['reports'][$l][$value_keys[$k]] = $line_item_value; + } + } + } + + $cache->set($cache_key, array( + 'timestamp' => time(), + 'result' => $result + )); + } else { + $result = $cache_result['result']; + } + + // Fill campaign statuses + foreach ($result['reports'] as &$report) { + if ($report['campaign_name'] == 'Total') { + $report['status'] = ''; + } else { + $report['status'] = $statuses[$report['campaign_name']]; + } + } + + $this->applyNewSetting('advertise_google_report_campaigns', $result); + } + + public function getProductReports($product_ids) { + $cache = new \Cache($this->config->get('cache_engine'), self::CACHE_PRODUCT_REPORT); + $cache_key = 'advertise_google.' . $this->store_id . '.product_reports.' . md5(json_encode($product_ids) . $this->setting->get('advertise_google_reporting_interval')); + + $cache_result = $cache->get($cache_key); + + if (!empty($cache_result['result']) && isset($cache_result['timestamp']) && (time() - self::CACHE_PRODUCT_REPORT <= $cache_result['timestamp'])) { + return $cache_result['result']; + } + + $post = array(); + $post_data = array( + 'product_ids' => $product_ids + ); + + $this->curlPostQuery($post_data, $post); + + $request = array( + 'type' => 'POST', + 'endpoint' => sprintf(self::ENDPOINT_REPORT_AD, $this->setting->get('advertise_google_reporting_interval')), + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => $post + ); + + $response = $this->api($request); + + $result = array(); + + if (!empty($response['ad_report'])) { + $lines = explode("\n", trim($response['ad_report'])); + + $header = explode(',', $lines[1]); + $data = array(); + $keys = array(); + + $expected = array( + 'Item Id' => 'offer_id', + 'Impressions' => 'impressions', + 'Clicks' => 'clicks', + 'Cost' => 'cost', + 'Conversions' => 'conversions', + 'Total conv. value' => 'conversion_value' + ); + + foreach ($header as $i => $title) { + if (!in_array($title, array_keys($expected))) { + continue; + } + + $data[$i] = 0.0; + $keys[$i] = $expected[$title]; + } + + // We want to omit the last line because it does not include the total number of impressions for all campaigns + for ($j = 2; $j < count($lines) - 1; $j++) { + $line_items = explode(',', $lines[$j]); + + $result[$j] = array(); + + foreach ($line_items as $k => $line_item) { + if (in_array($k, array_keys($data))) { + $result[$j][$keys[$k]] = (float)$line_item; + } + } + } + } + + $cache->set($cache_key, array( + 'result' => $result, + 'timestamp' => time() + )); + + return $result; + } + + public function getProductStatuses($product_ids) { + $post_data = array( + 'product_ids' => $product_ids + ); + + $this->curlPostQuery($post_data, $post); + + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_MERCHANT_PRODUCT_STATUSES, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => $post + ); + + $response = $this->api($request); + + return $response['statuses']; + } + + public function getConversionTracker() { + $request = array( + 'endpoint' => self::ENDPOINT_CONVERSION_TRACKER, + 'use_access_token' => true + ); + + $result = $this->api($request); + + // Amend the conversion snippet by replacing the default values with placeholders. + $search = array( + "'value': 0.0", + "'currency': 'USD'" + ); + + $replace = array( + "'value': {VALUE}", + "'currency': '{CURRENCY}'" + ); + + $result['conversion_tracker']['google_event_snippet'] = str_replace($search, $replace, $result['conversion_tracker']['google_event_snippet']); + + return $result['conversion_tracker']; + } + + public function testCampaigns() { + $request = array( + 'endpoint' => self::ENDPOINT_CAMPAIGN_TEST, + 'use_access_token' => true + ); + + $result = $this->api($request); + + return $result['status'] === true; + } + + public function testAccessToken() { + $request = array( + 'endpoint' => self::ENDPOINT_ACCESS_TOKEN_TEST, + 'use_access_token' => true + ); + + try { + $result = $this->api($request); + + return $result['status'] === true; + } catch (AccessForbiddenException $e) { + throw $e; + } catch (\RuntimeException $e) { + // Do nothing + } + + return false; + } + + public function getAccessToken() { + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_ACCESS_TOKEN, + 'use_access_token' => false, + 'content_type' => 'multipart/form-data', + 'data' => array( + 'grant_type' => 'refresh_token', + 'refresh_token' => $this->setting->get('advertise_google_refresh_token'), + 'client_id' => $this->setting->get('advertise_google_app_id'), + 'client_secret' => $this->setting->get('advertise_google_app_secret'), + 'scope' => self::SCOPES + ) + ); + + $access = $this->api($request); + + $this->applyNewSetting('advertise_google_access_token', $access['access_token']); + $this->applyNewSetting('advertise_google_refresh_token', $access['refresh_token']); + + return true; + } + + public function access($data, $code) { + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_ACCESS_TOKEN, + 'use_access_token' => false, + 'content_type' => 'multipart/form-data', + 'data' => array( + 'grant_type' => 'authorization_code', + 'client_id' => $data['app_id'], + 'client_secret' => $data['app_secret'], + 'redirect_uri' => $data['redirect_uri'], + 'code' => $code + ) + ); + + return $this->api($request); + } + + public function authorize($data) { + $query = array(); + + $query['response_type'] = 'code'; + $query['client_id'] = $data['app_id']; + $query['redirect_uri'] = $data['redirect_uri']; + $query['scope'] = self::SCOPES; + $query['state'] = $data['state']; + + return sprintf($this->endpoint_url, 'api/authorize/login') . '&' . http_build_query($query); + } + + public function verifySite() { + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_VERIFY_TOKEN, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => array( + 'url_website' => $this->store_url + ) + ); + + $response = $this->api($request); + + $token = $response['token']; + + $this->createVerificationToken($token); + + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_VERIFY_SITE, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => array( + 'url_website' => $this->store_url + ) + ); + + try { + $this->api($request); + + $this->deleteVerificationToken($token); + } catch (\RuntimeException $e) { + $this->deleteVerificationToken($token); + + throw $e; + } + } + + public function deleteCampaign($name) { + $post = array(); + $data = array( + 'delete' => array( + $name + ) + ); + + $this->curlPostQuery($data, $post); + + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_CAMPAIGN_DELETE, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => $post + ); + + $this->api($request); + } + + public function pushTargets() { + $post = array(); + $targets = array(); + + foreach ($this->getTargets($this->store_id) as $target) { + $targets[] = array( + 'campaign_name' => $target['campaign_name_raw'], + 'country' => $target['country']['code'], + 'status' => $this->setting->get('advertise_google_status') ? $target['status'] : 'paused', + 'budget' => (float)$target['budget']['value'], + 'roas' => ((int)$target['roas']) / 100, + 'feeds' => $target['feeds_raw'] + ); + } + + $data = array( + 'target' => $targets + ); + + $this->curlPostQuery($data, $post); + + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_CAMPAIGN_UPDATE, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => $post + ); + + $response = $this->api($request); + + $this->applyNewSetting('advertise_google_work', $response['work']); + } + + public function pushShippingAndTaxes() { + $post = array(); + $data = $this->setting->get('advertise_google_shipping_taxes'); + + $this->curlPostQuery($data, $post); + + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_MERCHANT_SHIPPING_TAXES, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => $post + ); + + $this->api($request); + } + + public function disconnect() { + $request = array( + 'type' => 'GET', + 'endpoint' => self::ENDPOINT_MERCHANT_DISCONNECT, + 'use_access_token' => true + ); + + $this->api($request); + } + + public function pushCampaignStatus() { + $post = array(); + $targets = array(); + + foreach ($this->getTargets($this->store_id) as $target) { + $targets[] = array( + 'campaign_name' => $target['campaign_name_raw'], + 'status' => $this->setting->get('advertise_google_status') ? $target['status'] : 'paused' + ); + } + + $data = array( + 'target' => $targets + ); + + $this->curlPostQuery($data, $post); + + $request = array( + 'type' => 'POST', + 'endpoint' => self::ENDPOINT_CAMPAIGN_STATUS, + 'use_access_token' => true, + 'content_type' => 'multipart/form-data', + 'data' => $post + ); + + $this->api($request); + } + + public function getAvailableCarriers() { + $request = array( + 'type' => 'GET', + 'endpoint' => self::ENDPOINT_MERCHANT_AVAILABLE_CARRIERS, + 'use_access_token' => true + ); + + $result = $this->api($request); + + return $result['available_carriers']; + } + + public function getLanguages($language_codes) { + $this->load->config('googleshopping/googleshopping'); + + $result = array(); + + foreach ($this->config->get('advertise_google_languages') as $code => $name) { + if (in_array($code, $language_codes)) { + $supported_language_id = $this->getSupportedLanguageId($code); + + $result[] = array( + 'status' => $supported_language_id !== 0, + 'language_id' => $supported_language_id, + 'code' => $code, + 'name' => $this->getLanguageName($supported_language_id, $name) + ); + } + } + + return $result; + } + + public function getLanguageName($language_id, $default) { + $this->load->model('localisation/language'); + + $language_info = $this->model_localisation_language->getLanguage($language_id); + + if (isset($language_info['name']) && trim($language_info['name']) != "") { + return $language_info['name']; + } + + // We do not expect to get to this point, but just in case... + return $default; + } + + public function getCurrencies($currency_codes) { + $result = array(); + + $this->load->config('googleshopping/googleshopping'); + + $result = array(); + + foreach ($this->config->get('advertise_google_currencies') as $code => $name) { + if (in_array($code, $currency_codes)) { + $supported_currency_id = $this->getSupportedCurrencyId($code); + + $result[] = array( + 'status' => $supported_currency_id !== 0, + 'code' => $code, + 'name' => $this->getCurrencyName($supported_currency_id, $name) . ' (' . $code . ')' + ); + } + } + + return $result; + } + + public function getCurrencyName($currency_id, $default) { + $this->load->model('extension/advertise/google'); + + $currency_info = $this->getCurrency($currency_id); + + if (isset($currency_info['title']) && trim($currency_info['title']) != "") { + return $currency_info['title']; + } + + // We do not expect to get to this point, but just in case... + return $default; + } + + public function getCurrency($currency_id) { + $query = $this->db->query("SELECT DISTINCT * FROM " . DB_PREFIX . "currency WHERE currency_id = '" . (int)$currency_id . "'"); + + return $query->row; + } + + public function debugLog($text) { + if ($this->setting->get('advertise_google_debug_log')) { + $this->debug_log->write($text); + } + } + + protected function target($target) { + $feeds_raw = json_decode($target['feeds'], true); + + $feeds = array_map(function($feed) { + $language = current($this->getLanguages(array($feed['language']))); + $currency = current($this->getCurrencies(array($feed['currency']))); + + return array( + 'text' => $language['name'] . ', ' . $currency['name'], + 'language' => $feed['language'], + 'currency' => $feed['currency'] + ); + }, $feeds_raw); + + return array( + 'target_id' => $target['advertise_google_target_id'], + 'campaign_name' => str_replace(',', ',', trim($target['campaign_name'])), + 'campaign_name_raw' => $target['campaign_name'], + 'country' => array( + 'code' => $target['country'], + 'name' => $this->getCountryName($target['country']) + ), + 'budget' => array( + 'formatted' => sprintf($this->language->get('text_per_day'), number_format((float)$target['budget'], 2)), + 'value' => (float)$target['budget'] + ), + 'feeds' => $feeds, + 'status' => $target['status'], + 'roas' => $target['roas'], + 'roas_status' => $target['date_added'] <= date('Y-m-d', time() - self::ROAS_WAIT_INTERVAL), + 'roas_available_on' => strtotime($target['date_added']) + self::ROAS_WAIT_INTERVAL, + 'feeds_raw' => $feeds_raw + ); + } + + private function curlPostQuery($arrays, &$new = array(), $prefix = null) { + foreach ($arrays as $key => $value) { + $k = isset($prefix) ? $prefix . '[' . $key . ']' : $key; + if (is_array($value)) { + $this->curlPostQuery($value, $new, $k); + } else { + $new[$k] = $value; + } + } + } + + private function createVerificationToken($token) { + $dir = dirname(DIR_SYSTEM); + + if (!is_dir($dir) || !is_writable($dir)) { + throw new \RuntimeException("Not a directory, or no permissions to write to: " . $dir); + } + + if (!file_put_contents($dir . '/' . $token, 'google-site-verification: ' . $token)) { + throw new \RuntimeException("Could not write to: " . $dir . '/' . $token); + } + } + + private function deleteVerificationToken($token) { + $dir = dirname(DIR_SYSTEM); + + if (!is_dir($dir) || !is_writable($dir)) { + throw new \RuntimeException("Not a directory, or no permissions to write to: " . $dir); + } + + $file = $dir . '/' . $token; + + if (is_file($file) && is_writable($file)) { + @unlink($file); + } + } + + private function applyNewSetting($key, $value) { + $sql = "SELECT * FROM `" . DB_PREFIX . "setting` WHERE `code`='advertise_google' AND `key`='" . $this->db->escape($key) . "'"; + $result = $this->db->query($sql); + + if (is_array($value)) { + $encoded = json_encode($value); + $serialized = 1; + } else { + $encoded = $value; + $serialized = 0; + } + + if ($result->num_rows == 0) { + $this->db->query("INSERT INTO `" . DB_PREFIX . "setting` SET `value`='" . $this->db->escape($encoded) . "', `code`='advertise_google', `key`='" . $this->db->escape($key) . "', serialized='" . $serialized . "', store_id='0'"); + + $this->setting->set($key, $value); + } else { + $this->db->query("UPDATE `" . DB_PREFIX . "setting` SET `value`='" . $this->db->escape($encoded) . "', serialized='" . $serialized . "' WHERE `code`='advertise_google' AND `key`='" . $this->db->escape($key) . "'"); + + $this->setting->set($key, $value); + } + } + + private function api($request) { + $this->debugLog("REQUEST: " . json_encode($request)); + + $url = sprintf($this->endpoint_url, $request['endpoint']); + + $headers = array(); + + if (isset($request['content_type'])) { + $headers[] = 'Content-Type: ' . $request['content_type']; + } else { + $headers[] = 'Content-Type: application/json'; + } + + if (!empty($request['use_access_token'])) { + $headers[] = 'Authorization: Bearer ' . $this->setting->get('advertise_google_access_token'); + } + + $curl_options = array(); + + if (isset($request['type']) && $request['type'] == 'POST') { + $curl_options[CURLOPT_POST] = true; + $curl_options[CURLOPT_POSTFIELDS] = $request['data']; + } + + $curl_options[CURLOPT_URL] = $url; + $curl_options[CURLOPT_RETURNTRANSFER] = true; + $curl_options[CURLOPT_HTTPHEADER] = $headers; + + $ch = curl_init(); + curl_setopt_array($ch, $curl_options); + $result = curl_exec($ch); + $info = curl_getinfo($ch); + curl_close($ch); + + $this->debugLog("RESPONSE: " . $result); + + if (!empty($result) && $info['http_code'] == 200) { + $return = json_decode($result, true); + + if ($return['error']) { + throw new \RuntimeException($return['message']); + } else { + return $return['result']; + } + } else if (in_array($info['http_code'], array(400, 401, 403))) { + $return = json_decode($result, true); + + if ($info['http_code'] != 401 && $return['error']) { + throw new \RuntimeException($return['message']); + } else { + throw new ConnectionException("Access unavailable. Please re-connect."); + } + } else if ($info['http_code'] == 402) { + $return = json_decode($result, true); + + if ($return['error']) { + throw new AccessForbiddenException($return['message']); + } else { + throw new ConnectionException("Access unavailable. Please re-connect."); + } + } else { + $this->debugLog("CURL ERROR! CURL INFO: " . print_r($info, true)); + + throw new \RuntimeException("A temporary error was encountered. Please try again later."); + } + } +} diff --git a/public/system/library/googleshopping/library.php b/public/system/library/googleshopping/library.php new file mode 100644 index 0000000..a9c244e --- /dev/null +++ b/public/system/library/googleshopping/library.php @@ -0,0 +1,19 @@ +<?php + +namespace googleshopping; + +abstract class Library { + protected $registry; + + public function __construct($registry) { + $this->registry = $registry; + } + + public function __get($key) { + return $this->registry->get($key); + } + + public function __set($key, $value) { + $this->registry->set($key, $value); + } +}
\ No newline at end of file diff --git a/public/system/library/googleshopping/log.php b/public/system/library/googleshopping/log.php new file mode 100644 index 0000000..74a5281 --- /dev/null +++ b/public/system/library/googleshopping/log.php @@ -0,0 +1,55 @@ +<?php + +namespace googleshopping; + +/** +* Log class +*/ +class Log { + private $handle; + + /** + * Constructor + * + * @param string $filename + */ + public function __construct($filename, $max_size = 8388608) { + $file = DIR_LOGS . $filename; + + clearstatcache(true); + + if ((!file_exists($file) && !is_writable(DIR_LOGS)) || (file_exists($file) && !is_writable($file))) { + // Do nothing, as we have no permissions + return; + } + + if (file_exists($file) && filesize($file) >= $max_size) { + $mode = 'wb'; + } else { + $mode = 'ab'; + } + + $this->handle = @fopen(DIR_LOGS . $filename, $mode); + } + + /** + * + * + * @param string $message + */ + public function write($message) { + if (is_resource($this->handle)) { + fwrite($this->handle, date('Y-m-d G:i:s') . ' - ' . print_r($message, true) . "\n"); + } + } + + /** + * + * + */ + public function __destruct() { + if (is_resource($this->handle)) { + fclose($this->handle); + } + } +} diff --git a/public/system/library/googleshopping/traits/libraryloader.php b/public/system/library/googleshopping/traits/libraryloader.php new file mode 100644 index 0000000..61bb7fa --- /dev/null +++ b/public/system/library/googleshopping/traits/libraryloader.php @@ -0,0 +1,11 @@ +<?php + +namespace googleshopping\traits; + +use \googleshopping\Googleshopping; + +trait LibraryLoader { + protected function loadLibrary($store_id) { + $this->registry->set('googleshopping', new Googleshopping($this->registry, $store_id)); + } +}
\ No newline at end of file diff --git a/public/system/library/googleshopping/traits/storeloader.php b/public/system/library/googleshopping/traits/storeloader.php new file mode 100644 index 0000000..fc1a591 --- /dev/null +++ b/public/system/library/googleshopping/traits/storeloader.php @@ -0,0 +1,15 @@ +<?php + +namespace googleshopping\traits; + +trait StoreLoader { + protected function loadStore($store_id) { + $this->registry->set('setting', new \Config()); + + $this->load->model('setting/setting'); + + foreach ($this->model_setting_setting->getSetting('advertise_google', $store_id) as $key => $value) { + $this->setting->set($key, $value); + } + } +}
\ No newline at end of file diff --git a/public/system/library/image.php b/public/system/library/image.php new file mode 100644 index 0000000..b1d7e9f --- /dev/null +++ b/public/system/library/image.php @@ -0,0 +1,339 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Image class +*/ +class Image { + private $file; + private $image; + private $width; + private $height; + private $bits; + private $mime; + + /** + * Constructor + * + * @param string $file + * + */ + public function __construct($file) { + if (!extension_loaded('gd')) { + exit('Error: PHP GD is not installed!'); + } + + if (file_exists($file)) { + $this->file = $file; + + $info = getimagesize($file); + + $this->width = $info[0]; + $this->height = $info[1]; + $this->bits = isset($info['bits']) ? $info['bits'] : ''; + $this->mime = isset($info['mime']) ? $info['mime'] : ''; + + if ($this->mime == 'image/gif') { + $this->image = imagecreatefromgif($file); + } elseif ($this->mime == 'image/png') { + $this->image = imagecreatefrompng($file); + } elseif ($this->mime == 'image/jpeg') { + $this->image = imagecreatefromjpeg($file); + } + } else { + exit('Error: Could not load image ' . $file . '!'); + } + } + + /** + * + * + * @return string + */ + public function getFile() { + return $this->file; + } + + /** + * + * + * @return array + */ + public function getImage() { + return $this->image; + } + + /** + * + * + * @return string + */ + public function getWidth() { + return $this->width; + } + + /** + * + * + * @return string + */ + public function getHeight() { + return $this->height; + } + + /** + * + * + * @return string + */ + public function getBits() { + return $this->bits; + } + + /** + * + * + * @return string + */ + public function getMime() { + return $this->mime; + } + + /** + * + * + * @param string $file + * @param int $quality + */ + public function save($file, $quality = 90) { + $info = pathinfo($file); + + $extension = strtolower($info['extension']); + + if (is_resource($this->image)) { + if ($extension == 'jpeg' || $extension == 'jpg') { + imagejpeg($this->image, $file, $quality); + } elseif ($extension == 'png') { + imagepng($this->image, $file); + } elseif ($extension == 'gif') { + imagegif($this->image, $file); + } + + imagedestroy($this->image); + } + } + + /** + * + * + * @param int $width + * @param int $height + * @param string $default + */ + public function resize($width = 0, $height = 0, $default = '') { + if (!$this->width || !$this->height) { + return; + } + + $xpos = 0; + $ypos = 0; + $scale = 1; + + $scale_w = $width / $this->width; + $scale_h = $height / $this->height; + + if ($default == 'w') { + $scale = $scale_w; + } elseif ($default == 'h') { + $scale = $scale_h; + } else { + $scale = min($scale_w, $scale_h); + } + + if ($scale == 1 && $scale_h == $scale_w && $this->mime != 'image/png') { + return; + } + + $new_width = (int)($this->width * $scale); + $new_height = (int)($this->height * $scale); + $xpos = (int)(($width - $new_width) / 2); + $ypos = (int)(($height - $new_height) / 2); + + $image_old = $this->image; + $this->image = imagecreatetruecolor($width, $height); + + if ($this->mime == 'image/png') { + imagealphablending($this->image, false); + imagesavealpha($this->image, true); + $background = imagecolorallocatealpha($this->image, 255, 255, 255, 127); + imagecolortransparent($this->image, $background); + } else { + $background = imagecolorallocate($this->image, 255, 255, 255); + } + + imagefilledrectangle($this->image, 0, 0, $width, $height, $background); + + imagecopyresampled($this->image, $image_old, $xpos, $ypos, 0, 0, $new_width, $new_height, $this->width, $this->height); + imagedestroy($image_old); + + $this->width = $width; + $this->height = $height; + } + + /** + * + * + * @param string $watermark + * @param string $position + */ + public function watermark($watermark, $position = 'bottomright') { + switch($position) { + case 'topleft': + $watermark_pos_x = 0; + $watermark_pos_y = 0; + break; + case 'topcenter': + $watermark_pos_x = intval(($this->width - $watermark->getWidth()) / 2); + $watermark_pos_y = 0; + break; + case 'topright': + $watermark_pos_x = $this->width - $watermark->getWidth(); + $watermark_pos_y = 0; + break; + case 'middleleft': + $watermark_pos_x = 0; + $watermark_pos_y = intval(($this->height - $watermark->getHeight()) / 2); + break; + case 'middlecenter': + $watermark_pos_x = intval(($this->width - $watermark->getWidth()) / 2); + $watermark_pos_y = intval(($this->height - $watermark->getHeight()) / 2); + break; + case 'middleright': + $watermark_pos_x = $this->width - $watermark->getWidth(); + $watermark_pos_y = intval(($this->height - $watermark->getHeight()) / 2); + break; + case 'bottomleft': + $watermark_pos_x = 0; + $watermark_pos_y = $this->height - $watermark->getHeight(); + break; + case 'bottomcenter': + $watermark_pos_x = intval(($this->width - $watermark->getWidth()) / 2); + $watermark_pos_y = $this->height - $watermark->getHeight(); + break; + case 'bottomright': + $watermark_pos_x = $this->width - $watermark->getWidth(); + $watermark_pos_y = $this->height - $watermark->getHeight(); + break; + } + + imagealphablending( $this->image, true ); + imagesavealpha( $this->image, true ); + imagecopy($this->image, $watermark->getImage(), $watermark_pos_x, $watermark_pos_y, 0, 0, $watermark->getWidth(), $watermark->getHeight()); + + imagedestroy($watermark->getImage()); + } + + /** + * + * + * @param int $top_x + * @param int $top_y + * @param int $bottom_x + * @param int $bottom_y + */ + public function crop($top_x, $top_y, $bottom_x, $bottom_y) { + $image_old = $this->image; + $this->image = imagecreatetruecolor($bottom_x - $top_x, $bottom_y - $top_y); + + imagecopy($this->image, $image_old, 0, 0, $top_x, $top_y, $this->width, $this->height); + imagedestroy($image_old); + + $this->width = $bottom_x - $top_x; + $this->height = $bottom_y - $top_y; + } + + /** + * + * + * @param int $degree + * @param string $color + */ + public function rotate($degree, $color = 'FFFFFF') { + $rgb = $this->html2rgb($color); + + $this->image = imagerotate($this->image, $degree, imagecolorallocate($this->image, $rgb[0], $rgb[1], $rgb[2])); + + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + } + + /** + * + * + */ + private function filter() { + $args = func_get_args(); + + call_user_func_array('imagefilter', $args); + } + + /** + * + * + * @param string $text + * @param int $x + * @param int $y + * @param int $size + * @param string $color + */ + private function text($text, $x = 0, $y = 0, $size = 5, $color = '000000') { + $rgb = $this->html2rgb($color); + + imagestring($this->image, $size, $x, $y, $text, imagecolorallocate($this->image, $rgb[0], $rgb[1], $rgb[2])); + } + + /** + * + * + * @param object $merge + * @param object $x + * @param object $y + * @param object $opacity + */ + private function merge($merge, $x = 0, $y = 0, $opacity = 100) { + imagecopymerge($this->image, $merge->getImage(), $x, $y, 0, 0, $merge->getWidth(), $merge->getHeight(), $opacity); + } + + /** + * + * + * @param string $color + * + * @return array + */ + private function html2rgb($color) { + if ($color[0] == '#') { + $color = substr($color, 1); + } + + if (strlen($color) == 6) { + list($r, $g, $b) = array($color[0] . $color[1], $color[2] . $color[3], $color[4] . $color[5]); + } elseif (strlen($color) == 3) { + list($r, $g, $b) = array($color[0] . $color[0], $color[1] . $color[1], $color[2] . $color[2]); + } else { + return false; + } + + $r = hexdec($r); + $g = hexdec($g); + $b = hexdec($b); + + return array($r, $g, $b); + } +}
\ No newline at end of file diff --git a/public/system/library/language.php b/public/system/library/language.php new file mode 100644 index 0000000..2e59668 --- /dev/null +++ b/public/system/library/language.php @@ -0,0 +1,85 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Language class +*/ +class Language { + private $default = 'en-gb'; + private $directory; + public $data = array(); + + /** + * Constructor + * + * @param string $file + * + */ + public function __construct($directory = '') { + $this->directory = $directory; + } + + /** + * + * + * @param string $key + * + * @return string + */ + public function get($key) { + return (isset($this->data[$key]) ? $this->data[$key] : $key); + } + + public function set($key, $value) { + $this->data[$key] = $value; + } + + /** + * + * + * @return array + */ + public function all() { + return $this->data; + } + + /** + * + * + * @param string $filename + * @param string $key + * + * @return array + */ + public function load($filename, $key = '') { + if (!$key) { + $_ = array(); + + $file = DIR_LANGUAGE . $this->default . '/' . $filename . '.php'; + + if (is_file($file)) { + require($file); + } + + $file = DIR_LANGUAGE . $this->directory . '/' . $filename . '.php'; + + if (is_file($file)) { + require($file); + } + + $this->data = array_merge($this->data, $_); + } else { + // Put the language into a sub key + $this->data[$key] = new Language($this->directory); + $this->data[$key]->load($filename); + } + + return $this->data; + } +}
\ No newline at end of file diff --git a/public/system/library/log.php b/public/system/library/log.php new file mode 100644 index 0000000..d32fb0f --- /dev/null +++ b/public/system/library/log.php @@ -0,0 +1,41 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Log class +*/ +class Log { + private $handle; + + /** + * Constructor + * + * @param string $filename + */ + public function __construct($filename) { + $this->handle = fopen(DIR_LOGS . $filename, 'a'); + } + + /** + * + * + * @param string $message + */ + public function write($message) { + fwrite($this->handle, date('Y-m-d G:i:s') . ' - ' . print_r($message, true) . "\n"); + } + + /** + * + * + */ + public function __destruct() { + fclose($this->handle); + } +}
\ No newline at end of file diff --git a/public/system/library/mail.php b/public/system/library/mail.php new file mode 100644 index 0000000..4e6c59c --- /dev/null +++ b/public/system/library/mail.php @@ -0,0 +1,144 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Mail class +*/ +class Mail { + protected $to; + protected $from; + protected $sender; + protected $reply_to; + protected $subject; + protected $text; + protected $html; + protected $attachments = array(); + public $parameter; + + /** + * Constructor + * + * @param string $adaptor + * + */ + public function __construct($adaptor = 'mail') { + $class = 'Mail\\' . $adaptor; + + if (class_exists($class)) { + $this->adaptor = new $class(); + } else { + trigger_error('Error: Could not load mail adaptor ' . $adaptor . '!'); + exit(); + } + } + + /** + * + * + * @param mixed $to + */ + public function setTo($to) { + $this->to = $to; + } + + /** + * + * + * @param string $from + */ + public function setFrom($from) { + $this->from = $from; + } + + /** + * + * + * @param string $sender + */ + public function setSender($sender) { + $this->sender = $sender; + } + + /** + * + * + * @param string $reply_to + */ + public function setReplyTo($reply_to) { + $this->reply_to = $reply_to; + } + + /** + * + * + * @param string $subject + */ + public function setSubject($subject) { + $this->subject = $subject; + } + + /** + * + * + * @param string $text + */ + public function setText($text) { + $this->text = $text; + } + + /** + * + * + * @param string $html + */ + public function setHtml($html) { + $this->html = $html; + } + + /** + * + * + * @param string $filename + */ + public function addAttachment($filename) { + $this->attachments[] = $filename; + } + + /** + * + * + */ + public function send() { + if (!$this->to) { + throw new \Exception('Error: E-Mail to required!'); + } + + if (!$this->from) { + throw new \Exception('Error: E-Mail from required!'); + } + + if (!$this->sender) { + throw new \Exception('Error: E-Mail sender required!'); + } + + if (!$this->subject) { + throw new \Exception('Error: E-Mail subject required!'); + } + + if ((!$this->text) && (!$this->html)) { + throw new \Exception('Error: E-Mail message required!'); + } + + foreach (get_object_vars($this) as $key => $value) { + $this->adaptor->$key = $value; + } + + $this->adaptor->send(); + } +}
\ No newline at end of file diff --git a/public/system/library/mail/mail.php b/public/system/library/mail/mail.php new file mode 100644 index 0000000..fe68a61 --- /dev/null +++ b/public/system/library/mail/mail.php @@ -0,0 +1,80 @@ +<?php +namespace Mail; +class Mail { + public function send() { + if (is_array($this->to)) { + $to = implode(',', $this->to); + } else { + $to = $this->to; + } + + $boundary = '----=_NextPart_' . md5(time()); + + $header = 'MIME-Version: 1.0' . PHP_EOL; + $header .= 'Date: ' . date('D, d M Y H:i:s O') . PHP_EOL; + $header .= 'From: =?UTF-8?B?' . base64_encode($this->sender) . '?= <' . $this->from . '>' . PHP_EOL; + + if (!$this->reply_to) { + $header .= 'Reply-To: =?UTF-8?B?' . base64_encode($this->sender) . '?= <' . $this->from . '>' . PHP_EOL; + } else { + $header .= 'Reply-To: =?UTF-8?B?' . base64_encode($this->reply_to) . '?= <' . $this->reply_to . '>' . PHP_EOL; + } + + $header .= 'Return-Path: ' . $this->from . PHP_EOL; + $header .= 'X-Mailer: PHP/' . phpversion() . PHP_EOL; + $header .= 'Content-Type: multipart/mixed; boundary="' . $boundary . '"' . PHP_EOL . PHP_EOL; + + if (!$this->html) { + $message = '--' . $boundary . PHP_EOL; + $message .= 'Content-Type: text/plain; charset="utf-8"' . PHP_EOL; + $message .= 'Content-Transfer-Encoding: 8bit' . PHP_EOL . PHP_EOL; + $message .= $this->text . PHP_EOL; + } else { + $message = '--' . $boundary . PHP_EOL; + $message .= 'Content-Type: multipart/alternative; boundary="' . $boundary . '_alt"' . PHP_EOL . PHP_EOL; + $message .= '--' . $boundary . '_alt' . PHP_EOL; + $message .= 'Content-Type: text/plain; charset="utf-8"' . PHP_EOL; + $message .= 'Content-Transfer-Encoding: 8bit' . PHP_EOL . PHP_EOL; + + if ($this->text) { + $message .= $this->text . PHP_EOL; + } else { + $message .= 'This is a HTML email and your email client software does not support HTML email!' . PHP_EOL; + } + + $message .= '--' . $boundary . '_alt' . PHP_EOL; + $message .= 'Content-Type: text/html; charset="utf-8"' . PHP_EOL; + $message .= 'Content-Transfer-Encoding: 8bit' . PHP_EOL . PHP_EOL; + $message .= $this->html . PHP_EOL; + $message .= '--' . $boundary . '_alt--' . PHP_EOL; + } + + foreach ($this->attachments as $attachment) { + if (file_exists($attachment)) { + $handle = fopen($attachment, 'r'); + + $content = fread($handle, filesize($attachment)); + + fclose($handle); + + $message .= '--' . $boundary . PHP_EOL; + $message .= 'Content-Type: application/octet-stream; name="' . basename($attachment) . '"' . PHP_EOL; + $message .= 'Content-Transfer-Encoding: base64' . PHP_EOL; + $message .= 'Content-Disposition: attachment; filename="' . basename($attachment) . '"' . PHP_EOL; + $message .= 'Content-ID: <' . urlencode(basename($attachment)) . '>' . PHP_EOL; + $message .= 'X-Attachment-Id: ' . urlencode(basename($attachment)) . PHP_EOL . PHP_EOL; + $message .= chunk_split(base64_encode($content)); + } + } + + $message .= '--' . $boundary . '--' . PHP_EOL; + + ini_set('sendmail_from', $this->from); + + if ($this->parameter) { + mail($to, '=?UTF-8?B?' . base64_encode($this->subject) . '?=', $message, $header, $this->parameter); + } else { + mail($to, '=?UTF-8?B?' . base64_encode($this->subject) . '?=', $message, $header); + } + } +}
\ No newline at end of file diff --git a/public/system/library/mail/smtp.php b/public/system/library/mail/smtp.php new file mode 100644 index 0000000..f5f822d --- /dev/null +++ b/public/system/library/mail/smtp.php @@ -0,0 +1,350 @@ +<?php +namespace Mail; +class Smtp { + public $smtp_hostname; + public $smtp_username; + public $smtp_password; + public $smtp_port = 25; + public $smtp_timeout = 5; + public $verp = false; + + public function send() { + if (is_array($this->to)) { + $to = implode(',', $this->to); + } else { + $to = $this->to; + } + + $boundary = '----=_NextPart_' . md5(time()); + + $header = 'MIME-Version: 1.0' . PHP_EOL; + $header .= 'To: <' . $to . '>' . PHP_EOL; + $header .= 'Subject: =?UTF-8?B?' . base64_encode($this->subject) . '?=' . PHP_EOL; + $header .= 'Date: ' . date('D, d M Y H:i:s O') . PHP_EOL; + $header .= 'From: =?UTF-8?B?' . base64_encode($this->sender) . '?= <' . $this->from . '>' . PHP_EOL; + + if (!$this->reply_to) { + $header .= 'Reply-To: =?UTF-8?B?' . base64_encode($this->sender) . '?= <' . $this->from . '>' . PHP_EOL; + } else { + $header .= 'Reply-To: =?UTF-8?B?' . base64_encode($this->reply_to) . '?= <' . $this->reply_to . '>' . PHP_EOL; + } + + $header .= 'Return-Path: ' . $this->from . PHP_EOL; + $header .= 'X-Mailer: PHP/' . phpversion() . PHP_EOL; + $header .= 'Content-Type: multipart/mixed; boundary="' . $boundary . '"' . PHP_EOL . PHP_EOL; + + if (!$this->html) { + $message = '--' . $boundary . PHP_EOL; + $message .= 'Content-Type: text/plain; charset="utf-8"' . PHP_EOL; + $message .= 'Content-Transfer-Encoding: 8bit' . PHP_EOL . PHP_EOL; + $message .= $this->text . PHP_EOL; + } else { + $message = '--' . $boundary . PHP_EOL; + $message .= 'Content-Type: multipart/alternative; boundary="' . $boundary . '_alt"' . PHP_EOL . PHP_EOL; + $message .= '--' . $boundary . '_alt' . PHP_EOL; + $message .= 'Content-Type: text/plain; charset="utf-8"' . PHP_EOL; + $message .= 'Content-Transfer-Encoding: 8bit' . PHP_EOL . PHP_EOL; + + if ($this->text) { + $message .= $this->text . PHP_EOL; + } else { + $message .= 'This is a HTML email and your email client software does not support HTML email!' . PHP_EOL; + } + + $message .= '--' . $boundary . '_alt' . PHP_EOL; + $message .= 'Content-Type: text/html; charset="utf-8"' . PHP_EOL; + $message .= 'Content-Transfer-Encoding: 8bit' . PHP_EOL . PHP_EOL; + $message .= $this->html . PHP_EOL; + $message .= '--' . $boundary . '_alt--' . PHP_EOL; + } + + foreach ($this->attachments as $attachment) { + if (file_exists($attachment)) { + $handle = fopen($attachment, 'r'); + + $content = fread($handle, filesize($attachment)); + + fclose($handle); + + $message .= '--' . $boundary . PHP_EOL; + $message .= 'Content-Type: application/octet-stream; name="' . basename($attachment) . '"' . PHP_EOL; + $message .= 'Content-Transfer-Encoding: base64' . PHP_EOL; + $message .= 'Content-Disposition: attachment; filename="' . basename($attachment) . '"' . PHP_EOL; + $message .= 'Content-ID: <' . urlencode(basename($attachment)) . '>' . PHP_EOL; + $message .= 'X-Attachment-Id: ' . urlencode(basename($attachment)) . PHP_EOL . PHP_EOL; + $message .= chunk_split(base64_encode($content)); + } + } + + $message .= '--' . $boundary . '--' . PHP_EOL; + + if (substr($this->smtp_hostname, 0, 3) == 'tls') { + $hostname = substr($this->smtp_hostname, 6); + } else { + $hostname = $this->smtp_hostname; + } + + $handle = fsockopen($hostname, $this->smtp_port, $errno, $errstr, $this->smtp_timeout); + + if (!$handle) { + throw new \Exception('Error: ' . $errstr . ' (' . $errno . ')'); + } else { + if (substr(PHP_OS, 0, 3) != 'WIN') { + socket_set_timeout($handle, $this->smtp_timeout, 0); + } + + while ($line = fgets($handle, 515)) { + if (substr($line, 3, 1) == ' ') { + break; + } + } + + fputs($handle, 'EHLO ' . getenv('SERVER_NAME') . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + //some SMTP servers respond with 220 code before responding with 250. hence, we need to ignore 220 response string + if (substr($reply, 0, 3) == 220 && substr($line, 3, 1) == ' ') { + $reply = ''; + continue; + } + else if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 250) { + throw new \Exception('Error: EHLO not accepted from server!'); + } + + if (substr($this->smtp_hostname, 0, 3) == 'tls') { + fputs($handle, 'STARTTLS' . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 220) { + throw new \Exception('Error: STARTTLS not accepted from server!'); + } + + stream_socket_enable_crypto($handle, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + } + + if (!empty($this->smtp_username) && !empty($this->smtp_password)) { + fputs($handle, 'EHLO ' . getenv('SERVER_NAME') . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 250) { + throw new \Exception('Error: EHLO not accepted from server!'); + } + + fputs($handle, 'AUTH LOGIN' . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 334) { + throw new \Exception('Error: AUTH LOGIN not accepted from server!'); + } + + fputs($handle, base64_encode($this->smtp_username) . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 334) { + throw new \Exception('Error: Username not accepted from server!'); + } + + fputs($handle, base64_encode($this->smtp_password) . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 235) { + throw new \Exception('Error: Password not accepted from server!'); + } + } else { + fputs($handle, 'HELO ' . getenv('SERVER_NAME') . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 250) { + throw new \Exception('Error: HELO not accepted from server!'); + } + } + + if ($this->verp) { + fputs($handle, 'MAIL FROM: <' . $this->from . '>XVERP' . "\r\n"); + } else { + fputs($handle, 'MAIL FROM: <' . $this->from . '>' . "\r\n"); + } + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 250) { + throw new \Exception('Error: MAIL FROM not accepted from server!'); + } + + if (!is_array($this->to)) { + fputs($handle, 'RCPT TO: <' . $this->to . '>' . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if ((substr($reply, 0, 3) != 250) && (substr($reply, 0, 3) != 251)) { + throw new \Exception('Error: RCPT TO not accepted from server!'); + } + } else { + foreach ($this->to as $recipient) { + fputs($handle, 'RCPT TO: <' . $recipient . '>' . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if ((substr($reply, 0, 3) != 250) && (substr($reply, 0, 3) != 251)) { + throw new \Exception('Error: RCPT TO not accepted from server!'); + } + } + } + + fputs($handle, 'DATA' . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 354) { + throw new \Exception('Error: DATA not accepted from server!'); + } + + // According to rfc 821 we should not send more than 1000 including the CRLF + $message = str_replace("\r\n", "\n", $header . $message); + $message = str_replace("\r", "\n", $message); + + $lines = explode("\n", $message); + + foreach ($lines as $line) { + $results = str_split($line, 998); + + foreach ($results as $result) { + if (substr(PHP_OS, 0, 3) != 'WIN') { + fputs($handle, $result . "\r\n"); + } else { + fputs($handle, str_replace("\n", "\r\n", $result) . "\r\n"); + } + } + } + + fputs($handle, '.' . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 250) { + throw new \Exception('Error: DATA not accepted from server!'); + } + + fputs($handle, 'QUIT' . "\r\n"); + + $reply = ''; + + while ($line = fgets($handle, 515)) { + $reply .= $line; + + if (substr($line, 3, 1) == ' ') { + break; + } + } + + if (substr($reply, 0, 3) != 221) { + throw new \Exception('Error: QUIT not accepted from server!'); + } + + fclose($handle); + } + } +}
\ No newline at end of file diff --git a/public/system/library/openbay.php b/public/system/library/openbay.php new file mode 100644 index 0000000..bcff9b9 --- /dev/null +++ b/public/system/library/openbay.php @@ -0,0 +1,452 @@ +<?php +final class Openbay { + private $registry; + private $installed_modules = array(); + public $installed_markets = array(); + private $logging = 1; + + public function __construct($registry) { + // OpenBay Pro + $this->registry = $registry; + + if ($this->db != null) { + $this->getInstalled(); + + foreach ($this->installed_markets as $market) { + $class = '\openbay\\'. ucfirst($market); + + $this->{$market} = new $class($registry); + } + } + + $this->logger = new \Log('openbay.log'); + } + + public function __get($name) { + return $this->registry->get($name); + } + + public function log($data, $write = true) { + if ($this->logging == 1) { + if (function_exists('getmypid')) { + $process_id = getmypid(); + $data = $process_id . ' - ' . $data; + } + + if ($write == true) { + $this->logger->write($data); + } + } + } + + public function encrypt($value, $key, $iv, $json = true) { + if ($json == true) { + $value = json_encode($value); + } + + return strtr(base64_encode(openssl_encrypt($value, 'aes-128-cbc', hash('sha256', hex2bin($key), true), 0, hex2bin($iv))), '+/=', '-_,'); + } + + public function decrypt($value, $key, $iv, $json = true) { + $response = trim(openssl_decrypt(base64_decode(strtr($value, '-_,', '+/=')), 'aes-128-cbc', hash('sha256', hex2bin($key), true), 0, hex2bin($iv))); + + if ($json == true) { + $response = json_decode($response, true); + } + + return $response; + } + + private function getInstalled() { + $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "extension WHERE `type` = 'openbay'"); + + foreach ($query->rows as $result) { + $this->installed_markets[] = $result['code']; + } + } + + public function getInstalledMarkets() { + return $this->installed_markets; + } + + public function putStockUpdateBulk($product_id_array, $end_inactive = false) { + /** + * putStockUpdateBulk + * + * Takes an array of product id's where stock has been modified + * + * @param $product_id_array + */ + + foreach ($this->installed_markets as $market) { + if ($this->config->get($market . '_status') == 1 || $this->config->get('openbay_' .$market . '_status') == 1) { + $this->{$market}->putStockUpdateBulk($product_id_array, $end_inactive); + } + } + } + + public function testDbColumn($table, $column) { + $res = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $table . "` LIKE '" . $column . "'"); + if($res->num_rows != 0) { + return true; + } else { + return false; + } + } + + public function testDbTable($table) { + $res = $this->db->query("SELECT `table_name` AS `c` FROM `information_schema`.`tables` WHERE `table_schema` = DATABASE()"); + + $tables = array(); + + foreach($res->rows as $row) { + $tables[] = $row['c']; + } + + if(in_array($table, $tables)) { + return true; + } else { + return false; + } + } + + public function splitName($name) { + $name = explode(' ', $name); + $fname = $name[0]; + unset($name[0]); + $lname = implode(' ', $name); + + return array( + 'firstname' => $fname, + 'surname' => $lname + ); + } + + public function getTaxRates($tax_class_id) { + $tax_rates = array(); + + $tax_query = $this->db->query("SELECT + tr2.tax_rate_id, + tr2.name, + tr2.rate, + tr2.type, + tr1.priority + FROM " . DB_PREFIX . "tax_rule tr1 + LEFT JOIN " . DB_PREFIX . "tax_rate tr2 ON (tr1.tax_rate_id = tr2.tax_rate_id) + INNER JOIN " . DB_PREFIX . "tax_rate_to_customer_group tr2cg ON (tr2.tax_rate_id = tr2cg.tax_rate_id) + LEFT JOIN " . DB_PREFIX . "zone_to_geo_zone z2gz ON (tr2.geo_zone_id = z2gz.geo_zone_id) + LEFT JOIN " . DB_PREFIX . "geo_zone gz ON (tr2.geo_zone_id = gz.geo_zone_id) + WHERE tr1.tax_class_id = '" . (int)$tax_class_id . "' + AND tr1.based = 'shipping' + AND tr2cg.customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "' + AND z2gz.country_id = '" . (int)$this->config->get('config_country_id') . "' + AND (z2gz.zone_id = '0' OR z2gz.zone_id = '" . (int)$this->config->get('config_zone_id') . "') + ORDER BY tr1.priority ASC"); + + foreach ($tax_query->rows as $result) { + $tax_rates[$result['tax_rate_id']] = array( + 'tax_rate_id' => $result['tax_rate_id'], + 'name' => $result['name'], + 'rate' => $result['rate'], + 'type' => $result['type'], + 'priority' => $result['priority'] + ); + } + + return $tax_rates; + } + + public function getTaxRate($class_id) { + $rates = $this->getTaxRates($class_id); + $percentage = 0.00; + foreach($rates as $rate) { + if($rate['type'] == 'P') { + $percentage += $rate['rate']; + } + } + + return $percentage; + } + + public function getZoneId($name, $country_id) { + $query = $this->db->query("SELECT `zone_id` FROM `" . DB_PREFIX . "zone` WHERE `country_id` = '" . (int)$country_id . "' AND status = '1' AND `name` = '" . $this->db->escape($name) . "'"); + + if($query->num_rows > 0) { + return $query->row['zone_id']; + } else { + return 0; + } + } + + public function newOrderAdminNotify($order_id, $order_status_id) { + $order_info = $this->model_checkout_order->getOrder($order_id); + + if ($order_info && !$order_info['order_status_id'] && $order_status_id && in_array('order', (array)$this->config->get('config_mail_alert'))) { + $this->load->language('mail/order_alert'); + + // HTML Mail + $data['text_received'] = $this->language->get('text_received'); + $data['text_order_id'] = $this->language->get('text_order_id'); + $data['text_date_added'] = $this->language->get('text_date_added'); + $data['text_order_status'] = $this->language->get('text_order_status'); + $data['text_product'] = $this->language->get('text_product'); + $data['text_total'] = $this->language->get('text_total'); + $data['text_comment'] = $this->language->get('text_comment'); + + $data['order_id'] = $order_info['order_id']; + $data['date_added'] = date($this->language->get('date_format_short'), strtotime($order_info['date_added'])); + + $order_status_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "order_status WHERE order_status_id = '" . (int)$order_status_id . "' AND language_id = '" . (int)$this->config->get('config_language_id') . "'"); + + if ($order_status_query->num_rows) { + $data['order_status'] = $order_status_query->row['name']; + } else { + $data['order_status'] = ''; + } + + $data['store_url'] = HTTP_SERVER; + $data['store'] = html_entity_decode($this->config->get('config_name'), ENT_QUOTES, 'UTF-8'); + + $this->load->model('tool/image'); + + if (is_file(DIR_IMAGE . $this->config->get('config_logo'))) { + $data['logo'] = $this->model_tool_image->resize($this->config->get('config_logo'), $this->config->get('theme_default_image_location_width'), $this->config->get('theme_default_image_cart_height')); + } else { + $data['logo'] = ''; + } + + $this->load->model('tool/upload'); + + $data['products'] = array(); + + $order_products = $this->model_checkout_order->getOrderProducts($order_id); + + foreach ($order_products as $order_product) { + $option_data = array(); + + $order_options = $this->model_checkout_order->getOrderOptions($order_info['order_id'], $order_product['order_product_id']); + + foreach ($order_options as $order_option) { + if ($order_option['type'] != 'file') { + $value = $order_option['value']; + } else { + $upload_info = $this->model_tool_upload->getUploadByCode($order_option['value']); + + if ($upload_info) { + $value = $upload_info['name']; + } else { + $value = ''; + } + } + + $option_data[] = array( + 'name' => $order_option['name'], + 'value' => (utf8_strlen($value) > 20 ? utf8_substr($value, 0, 20) . '..' : $value) + ); + } + + $data['products'][] = array( + 'name' => $order_product['name'], + 'model' => $order_product['model'], + 'quantity' => $order_product['quantity'], + 'option' => $option_data, + 'total' => html_entity_decode($this->currency->format($order_product['total'] + ($this->config->get('config_tax') ? ($order_product['tax'] * $order_product['quantity']) : 0), $order_info['currency_code'], $order_info['currency_value']), ENT_NOQUOTES, 'UTF-8') + ); + } + + $data['vouchers'] = array(); + + $order_vouchers = $this->model_checkout_order->getOrderVouchers($order_id); + + foreach ($order_vouchers as $order_voucher) { + $data['vouchers'][] = array( + 'description' => $order_voucher['description'], + 'amount' => html_entity_decode($this->currency->format($order_voucher['amount'], $order_info['currency_code'], $order_info['currency_value']), ENT_NOQUOTES, 'UTF-8') + ); + } + + $data['totals'] = array(); + + $order_totals = $this->model_checkout_order->getOrderTotals($order_id); + + foreach ($order_totals as $order_total) { + $data['totals'][] = array( + 'title' => $order_total['title'], + 'value' => html_entity_decode($this->currency->format($order_total['value'], $order_info['currency_code'], $order_info['currency_value']), ENT_NOQUOTES, 'UTF-8') + ); + } + + $data['comment'] = strip_tags($order_info['comment']); + + $mail = new Mail($this->config->get('config_mail_engine')); + $mail->parameter = $this->config->get('config_mail_parameter'); + $mail->smtp_hostname = $this->config->get('config_mail_smtp_hostname'); + $mail->smtp_username = $this->config->get('config_mail_smtp_username'); + $mail->smtp_password = html_entity_decode($this->config->get('config_mail_smtp_password'), ENT_QUOTES, 'UTF-8'); + $mail->smtp_port = $this->config->get('config_mail_smtp_port'); + $mail->smtp_timeout = $this->config->get('config_mail_smtp_timeout'); + + $mail->setTo($this->config->get('config_email')); + $mail->setFrom($this->config->get('config_email')); + $mail->setSender(html_entity_decode($order_info['store_name'], ENT_QUOTES, 'UTF-8')); + $mail->setSubject(html_entity_decode(sprintf($this->language->get('text_subject'), $this->config->get('config_name'), $order_info['order_id']), ENT_QUOTES, 'UTF-8')); + $mail->setText($this->load->view('mail/order_alert', $data)); + $mail->send(); + + // Send to additional alert emails + $emails = explode(',', $this->config->get('config_mail_alert_email')); + + foreach ($emails as $email) { + if ($email && filter_var($email, FILTER_VALIDATE_EMAIL)) { + $mail->setTo($email); + $mail->send(); + } + } + } + } + + public function orderDelete($order_id) { + /** + * Called when an order is deleted in the admin + * Use it to add stock back to the marketplaces + */ + foreach ($this->installed_markets as $market) { + if ($this->config->get($market . '_status') == 1 || $this->config->get('openbay_' .$market . '_status') == 1) { + $this->{$market}->orderDelete($order_id); + } + } + } + + public function getProductModelNumber($product_id, $sku = null) { + if($sku != null) { + $qry = $this->db->query("SELECT `sku` FROM `" . DB_PREFIX . "product_option_variant` WHERE `product_id` = '" . (int)$product_id . "' AND `sku` = '" . $this->db->escape($sku) . "'"); + + if($qry->num_rows > 0) { + return $qry->row['sku']; + } else { + return false; + } + } else { + $qry = $this->db->query("SELECT `model` FROM `" . DB_PREFIX . "product` WHERE `product_id` = '" . (int)$product_id . "' LIMIT 1"); + + if($qry->num_rows > 0) { + return $qry->row['model']; + } else { + return false; + } + } + } + + public function getProductTaxClassId($product_id) { + $qry = $this->db->query("SELECT `tax_class_id` FROM `" . DB_PREFIX . "product` WHERE `product_id` = '" . (int)$product_id . "' LIMIT 1"); + + if($qry->num_rows > 0) { + return $qry->row['tax_class_id']; + } else { + return false; + } + } + + public function addonLoad($addon) { + $addon = strtolower((string)$addon); + + if (empty($this->installed_modules)) { + $this->installed_modules = array(); + + $rows = $this->db->query("SELECT `code` FROM " . DB_PREFIX . "extension")->rows; + + foreach ($rows as $row) { + $this->installed_modules[] = strtolower($row['code']); + } + } + + return in_array($addon, $this->installed_modules); + } + + public function getUserByEmail($email) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "customer` WHERE `email` = '" . $this->db->escape($email) . "'"); + + if($qry->num_rows){ + return $qry->row['customer_id']; + } else { + return false; + } + } + + public function getProductOptions($product_id) { + $product_option_data = array(); + + $product_option_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "product_option po LEFT JOIN `" . DB_PREFIX . "option` o ON (po.option_id = o.option_id) LEFT JOIN " . DB_PREFIX . "option_description od ON (o.option_id = od.option_id) WHERE po.product_id = '" . (int)$product_id . "' AND od.language_id = '" . (int)$this->config->get('config_language_id') . "' ORDER BY o.sort_order"); + + foreach ($product_option_query->rows as $product_option) { + if ($product_option['type'] == 'select' || $product_option['type'] == 'radio' || $product_option['type'] == 'checkbox' || $product_option['type'] == 'image') { + $product_option_value_data = array(); + + $product_option_value_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "product_option_value pov LEFT JOIN " . DB_PREFIX . "option_value ov ON (pov.option_value_id = ov.option_value_id) LEFT JOIN " . DB_PREFIX . "option_value_description ovd ON (ov.option_value_id = ovd.option_value_id) WHERE pov.product_option_id = '" . (int)$product_option['product_option_id'] . "' AND ovd.language_id = '" . (int)$this->config->get('config_language_id') . "' ORDER BY ov.sort_order"); + + foreach ($product_option_value_query->rows as $product_option_value) { + $product_option_value_data[] = array( + 'product_option_value_id' => $product_option_value['product_option_value_id'], + 'option_value_id' => $product_option_value['option_value_id'], + 'name' => $product_option_value['name'], + 'image' => $product_option_value['image'], + 'quantity' => $product_option_value['quantity'], + 'subtract' => $product_option_value['subtract'], + 'price' => $product_option_value['price'], + 'price_prefix' => $product_option_value['price_prefix'], + 'points' => $product_option_value['points'], + 'points_prefix' => $product_option_value['points_prefix'], + 'weight' => $product_option_value['weight'], + 'weight_prefix' => $product_option_value['weight_prefix'] + ); + } + + $product_option_data[] = array( + 'product_option_id' => $product_option['product_option_id'], + 'option_id' => $product_option['option_id'], + 'name' => $product_option['name'], + 'type' => $product_option['type'], + 'product_option_value' => $product_option_value_data, + 'required' => $product_option['required'] + ); + } else { + $product_option_data[] = array( + 'product_option_id' => $product_option['product_option_id'], + 'option_id' => $product_option['option_id'], + 'name' => $product_option['name'], + 'type' => $product_option['type'], + 'option_value' => $product_option['value'], + 'required' => $product_option['required'] + ); + } + } + + return $product_option_data; + } + + public function getOrderProducts($order_id) { + $order_products = $this->db->query("SELECT `product_id`, `order_product_id` FROM `" . DB_PREFIX . "order_product` WHERE `order_id` = '" . (int)$order_id . "'"); + + if($order_products->num_rows > 0) { + return $order_products->rows; + } else { + return array(); + } + } + + public function getOrderProductVariant($order_id, $product_id, $order_product_id) { + $this->load->model('extension/module/openstock'); + + $order_option_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "order_option WHERE order_id = '" . (int)$order_id . "' AND order_product_id = '" . (int)$order_product_id . "'"); + + if ($order_option_query->num_rows) { + $options = array(); + + foreach ($order_option_query->rows as $option) { + $options[] = $option['product_option_value_id']; + } + + return $this->model_extension_module_openstock->getVariantByOptionValues($options, $product_id); + } + } +} diff --git a/public/system/library/openbay/amazon.php b/public/system/library/openbay/amazon.php new file mode 100644 index 0000000..f627180 --- /dev/null +++ b/public/system/library/openbay/amazon.php @@ -0,0 +1,534 @@ +<?php +namespace openbay; + +final class Amazon { + private $token; + private $encryption_key; + private $encryption_iv; + private $url = 'https://uk-amazon.openbaypro.com/'; + private $registry; + + public function __construct($registry) { + $this->registry = $registry; + $this->token = $this->config->get('openbay_amazon_token'); + + $this->setEncryptionKey($this->config->get('openbay_amazon_encryption_key')); + $this->setEncryptionIv($this->config->get('openbay_amazon_encryption_iv')); + } + + public function __get($name) { + return $this->registry->get($name); + } + + public function getEncryptionKey() { + return $this->encryption_key; + } + + public function setEncryptionKey($key) { + $this->encryption_key = $key; + } + + public function getEncryptionIv() { + return $this->encryption_iv; + } + + public function setEncryptionIv($encryption_iv) { + $this->encryption_iv = $encryption_iv; + } + + public function call($method, $data = array(), $use_json = true) { + if (!empty($data)) { + if ($use_json) { + $string = json_encode($data); + } else { + $string = $data; + } + + $encrypted = $this->openbay->encrypt($string, $this->getEncryptionKey(), $this->getEncryptionIv(), false); + } else { + $encrypted = ''; + } + + $post_data = array( + 'token' => $this->token, + 'data' => base64_encode($encrypted), + 'opencart_version' => VERSION + ); + + $headers = array(); + $headers[] = 'X-Endpoint-Version: 2'; + + $defaults = array( + CURLOPT_HEADER => 0, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POST => 1, + CURLOPT_URL => $this->url . $method, + CURLOPT_USERAGENT => 'OpenBay Pro for Amazon/Opencart', + CURLOPT_FRESH_CONNECT => 1, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FORBID_REUSE => 1, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_POSTFIELDS => http_build_query($post_data, '', "&"), + ); + + $curl = curl_init(); + + curl_setopt_array($curl, $defaults); + + $response = curl_exec($curl); + + curl_close($curl); + + return $response; + } + + public function callNoResponse($method, $data = array(), $use_json = true) { + if (!empty($data)) { + if ($use_json) { + $string = json_encode($data); + } else { + $string = $data; + } + + $encrypted = $this->openbay->encrypt($string, $this->getEncryptionKey(), $this->getEncryptionIv(), false); + } else { + $encrypted = ''; + } + + $post_data = array( + 'token' => $this->token, + 'data' => base64_encode($encrypted), + 'opencart_version' => VERSION + ); + + $headers = array(); + $headers[] = 'X-Endpoint-Version: 2'; + + $defaults = array( + CURLOPT_HEADER => 0, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POST => 1, + CURLOPT_URL => $this->url . $method, + CURLOPT_USERAGENT => 'OpenBay Pro for Amazon/Opencart', + CURLOPT_FRESH_CONNECT => 1, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FORBID_REUSE => 1, + CURLOPT_TIMEOUT => 2, + CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_POSTFIELDS => http_build_query($post_data, '', "&"), + ); + $curl = curl_init(); + + curl_setopt_array($curl, $defaults); + + curl_exec($curl); + + curl_close($curl); + } + + public function getServer() { + return $this->url; + } + + public function productUpdateListen($product_id, $data = array()) { + $logger = new \Log('amazon_stocks.log'); + $logger->write('productUpdateListen(), product ID: ' . $product_id); + + $product = $this->db->query("SELECT DISTINCT * FROM `" . DB_PREFIX . "product` WHERE `product_id` = '" . (int)$product_id . "' LIMIT 1")->row; + + if ($this->openbay->addonLoad('openstock') && (isset($product['has_option']) && $product['has_option'] == 1)) { + $this->load->model('extension/module/openstock'); + $logger->write('Variant item'); + + $quantity_data = array(); + + // check if post data['variant'], if not then call db to get variants + if (!isset($data['variant'])) { + $variants = $this->model_extension_module_openstock->getVariants($product_id); + } else { + $variants = $data['variant']; + } + + foreach ($variants as $variant) { + $amazon_sku_rows = $this->db->query("SELECT `amazon_sku` FROM `" . DB_PREFIX . "amazon_product_link` WHERE `product_id` = '" . (int)$product_id . "' AND `var` = '" . $this->db->escape($variant['sku']) . "'")->rows; + + foreach($amazon_sku_rows as $amazon_sku_row) { + $quantity_data[$amazon_sku_row['amazon_sku']] = $variant['stock']; + } + } + + if(!empty($quantity_data)) { + $logger->write('Updating with: ' . print_r($quantity_data, true)); + $this->updateQuantities($quantity_data); + } else { + $logger->write('Not required.'); + } + } else { + $this->putStockUpdateBulk(array($product_id)); + } + + $logger->write('productUpdateListen() - finished'); + } + + public function bulkUpdateOrders($orders) { + // Is the module enabled and called from admin? + if ($this->config->get('openbay_amazon_status') != 1 || !defined('HTTPS_CATALOG')) { + return; + } + $this->load->model('extension/openbay/amazon'); + + $log = new \Log('amazon.log'); + $log->write('Called bulkUpdateOrders method'); + + $request = array( + 'orders' => array(), + ); + + foreach ($orders as $order) { + $amazon_order = $this->getOrder($order['order_id']); + $amazon_order_products = $this->model_extension_openbay_amazon->getAmazonOrderedProducts($order['order_id']); + + $products = array(); + + foreach ($amazon_order_products as $amazon_order_product) { + $products[] = array( + 'amazon_order_item_id' => $amazon_order_product['amazon_order_item_id'], + 'quantity' => $amazon_order_product['quantity'], + ); + } + + $order_info = array( + 'amazon_order_id' => $amazon_order['amazon_order_id'], + 'status' => $order['status'], + 'products' => $products, + ); + + if ($order['status'] == 'shipped' && !empty($order['carrier'])) { + if ($order['carrier_from_list']) { + $order_info['carrier_id'] = $order['carrier']; + } else { + $order_info['carrier_name'] = $order['carrier']; + } + + $order_info['tracking'] = $order['tracking']; + } + + $request['orders'][] = $order_info; + } + + $log->write('order/bulkUpdate call: ' . print_r($request, 1)); + + $response = $this->call('order/bulkUpdate', $request); + + $log->write('order/bulkUpdate response: ' . $response); + } + + public function updateOrder($order_id, $order_status_string, $courier_id = '', $courier_from_list = true, $tracking_no = '') { + if ($this->config->get('openbay_amazon_status') != 1) { + return; + } + + /* Is called from admin? */ + if (!defined('HTTPS_CATALOG')) { + return; + } + + $amazon_order = $this->getOrder($order_id); + + if(!$amazon_order) { + return; + } + + $amazon_order_id = $amazon_order['amazon_order_id']; + + $log = new \Log('amazon.log'); + $log->write("Order's $amazon_order_id status changed to $order_status_string"); + + $this->load->model('extension/openbay/amazon'); + $amazon_order_products = $this->model_extension_openbay_amazon->getAmazonOrderedProducts($order_id); + + $request_node = new \SimpleXMLElement('<Request/>'); + + $request_node->addChild('AmazonOrderId', $amazon_order_id); + $request_node->addChild('Status', $order_status_string); + + if(!empty($courier_id)) { + if($courier_from_list) { + $request_node->addChild('CourierId', $courier_id); + } else { + $request_node->addChild('CourierOther', $courier_id); + } + $request_node->addChild('TrackingNo', $tracking_no); + } + + $order_items_node = $request_node->addChild('OrderItems'); + + foreach ($amazon_order_products as $product) { + $new_order_item = $order_items_node->addChild('OrderItem'); + $new_order_item->addChild('ItemId', htmlspecialchars($product['amazon_order_item_id'])); + $new_order_item->addChild('Quantity', (int)$product['quantity']); + } + + $doc = new \DOMDocument('1.0'); + $doc->preserveWhiteSpace = false; + $doc->loadXML($request_node->asXML()); + $doc->formatOutput = true; + + $this->model_extension_openbay_amazon->updateAmazonOrderTracking($order_id, $courier_id, $courier_from_list, !empty($courier_id) ? $tracking_no : ''); + $log->write('Request: ' . $doc->saveXML()); + + $response = $this->call('order/update2', $doc->saveXML(), false); + + $log->write("Response for Order's status update: $response"); + } + + public function getCategoryTemplates() { + $result = $this->call("productv2/RequestTemplateList", array('list' => true)); + if(isset($result)) { + return (array)json_decode($result); + } else { + return array(); + } + } + + public function registerInsertion($data) { + $result = $this->call("productv2/RegisterInsertionRequest", $data); + if(isset($result)) { + return (array)json_decode($result); + } else { + return array(); + } + } + + public function insertProduct($data) { + $result = $this->call("productv2/InsertProductRequest", $data); + if(isset($result)) { + return (array)json_decode($result); + } else { + return array(); + } + } + + public function updateQuantities($data) { + $result = $this->call("product/UpdateQuantityRequest", $data); + if(isset($result)) { + return (array)json_decode($result); + } else { + return array(); + } + } + + public function getStockUpdatesStatus($data) { + $result = $this->call("status/StockUpdates", $data); + if(isset($result)) { + return $result; + } else { + return false; + } + } + + public function putStockUpdateBulk($product_id_array, $end_inactive = false){ + $logger = new \Log('amazon_stocks.log'); + $logger->write('putStockUpdateBulk(), End inactive: ' . (int)$end_inactive . ', ids: ' . json_encode($product_id_array)); + + $quantity_data = array(); + + foreach($product_id_array as $product_id) { + $linked_skus = $this->db->query("SELECT `amazon_sku` FROM `" . DB_PREFIX . "amazon_product_link` WHERE `product_id` = '" . (int)$product_id . "'")->rows; + + if (!empty($linked_skus)) { + foreach ($linked_skus as $sku) { + $product = $this->db->query("SELECT `quantity`, `status` FROM `" . DB_PREFIX . "product` WHERE `product_id` = '" . (int)$product_id . "'")->row; + + if (!empty($product)) { + if ($end_inactive && $product['status'] == '0') { + $quantity_data[$sku['amazon_sku']] = 0; + } else { + $quantity_data[$sku['amazon_sku']] = $product['quantity']; + } + } + } + } else { + $logger->write('No linked SKU'); + } + } + + if(!empty($quantity_data)) { + $logger->write('New Qty:' . print_r($quantity_data, true)); + + $response = $this->updateQuantities($quantity_data); + + $logger->write('API Response: ' . print_r($response, true)); + } else { + $logger->write('No update needed'); + } + } + + public function getOrderdProducts($order_id) { + return $this->db->query("SELECT `op`.`product_id`, `p`.`quantity` as `quantity_left` FROM `" . DB_PREFIX . "order_product` as `op` LEFT JOIN `" . DB_PREFIX . "product` as `p` ON `p`.`product_id` = `op`.`product_id` WHERE `op`.`order_id` = '" . (int)$order_id . "'")->rows; + } + + public function validate() { + if($this->config->get('openbay_amazon_status') != 0 && + $this->config->get('openbay_amazon_token') != '' && + $this->config->get('openbay_amazon_encryption_key') != '' && + $this->config->get('openbay_amazon_encryption_iv') != ''){ + return true; + } else { + return false; + } + } + + public function deleteProduct($product_id){ + $this->db->query("DELETE FROM `" . DB_PREFIX . "amazon_product_link` WHERE `product_id` = '" . (int)$product_id . "'"); + } + + public function orderDelete($order_id){ + /** + * @todo + */ + } + + public function getOrder($order_id) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "amazon_order` WHERE `order_id` = '" . (int)$order_id . "' LIMIT 1"); + + if($qry->num_rows > 0) { + return $qry->row; + } else { + return false; + } + } + + public function getCarriers() { + return array( + "USPS", + "UPS", + "UPSMI", + "FedEx", + "DHL", + "Fastway", + "GLS", + "GO!", + "Hermes Logistik Gruppe", + "Royal Mail", + "Parcelforce", + "City Link", + "TNT", + "Target", + "SagawaExpress", + "NipponExpress", + "YamatoTransport", + "DHL Global Mail", + "UPS Mail Innovations", + "FedEx SmartPost", + "OSM", + "OnTrac", + "Streamlite", + "Newgistics", + "Canada Post", + "Blue Package", + "Chronopost", + "Deutsche Post", + "DPD", + "La Poste", + "Parcelnet", + "Poste Italiane", + "SDA", + "Smartmail", + "FEDEX_JP", + "JP_EXPRESS", + "NITTSU", + "SAGAWA", + "YAMATO", + "BlueDart", + "AFL/Fedex", + "Aramex", + "India Post", + "Professional", + "DTDC", + "Overnite Express", + "First Flight", + "Delhivery", + "Lasership", + ); + } + + public function parseCategoryTemplate($xml) { + $simplexml = null; + + libxml_use_internal_errors(true); + if(($simplexml = simplexml_load_string($xml)) == false) { + return false; + } + + $category = (string)$simplexml->filename; + + $tabs = array(); + foreach($simplexml->tabs->tab as $tab) { + $attributes = $tab->attributes(); + $tabs[] = array( + 'id' => (string)$attributes['id'], + 'name' => (string)$tab->name, + ); + } + + $fields = array(); + $field_types = array('required', 'desired', 'optional'); + foreach ($field_types as $type) { + foreach ($simplexml->fields->$type->field as $field) { + $attributes = $field->attributes(); + $fields[] = array( + 'name' => (string)$attributes['name'], + 'title' => (string)$field->title, + 'definition' => (string)$field->definition, + 'accepted' => (array)$field->accepted, + 'type' => (string)$type, + 'child' => false, + 'order' => (isset($attributes['order'])) ? (string)$attributes['order'] : '', + 'tab' => (string)$attributes['tab'], + ); + } + foreach ($simplexml->fields->$type->childfield as $field) { + $attributes = $field->attributes(); + $fields[] = array( + 'name' => (string)$attributes['name'], + 'title' => (string)$field->title, + 'definition' => (string)$field->definition, + 'accepted' => (array)$field->accepted, + 'type' => (string)$type, + 'child' => true, + 'parent' => (array)$field->parent, + 'order' => (isset($attributes['order'])) ? (string)$attributes['order'] : '', + 'tab' => (string)$attributes['tab'], + ); + } + } + + foreach($fields as $index => $field) { + $fields[$index]['unordered_index'] = $index; + } + + usort($fields, array('openbay\Amazon','compareFields')); + + return array( + 'category' => $category, + 'fields' => $fields, + 'tabs' => $tabs, + ); + } + + private static function compareFields($field1, $field2) { + if($field1['order'] == $field2['order']) { + return ($field1['unordered_index'] < $field2['unordered_index']) ? -1 : 1; + } else if(!empty($field1['order']) && empty($field2['order'])) { + return -1; + } else if(!empty($field2['order']) && empty($field1['order'])) { + return 1; + } else { + return ($field1['order'] < $field2['order']) ? -1 : 1; + } + } +} diff --git a/public/system/library/openbay/amazonus.php b/public/system/library/openbay/amazonus.php new file mode 100644 index 0000000..b8ac34b --- /dev/null +++ b/public/system/library/openbay/amazonus.php @@ -0,0 +1,509 @@ +<?php +namespace openbay; + +final class Amazonus { + private $token; + private $encryption_key; + private $encryption_iv; + private $url = 'https://us-amazon.openbaypro.com/'; + private $registry; + + public function __construct($registry) { + $this->registry = $registry; + $this->token = $this->config->get('openbay_amazonus_token'); + + $this->setEncryptionKey($this->config->get('openbay_amazonus_encryption_key')); + $this->setEncryptionIv($this->config->get('openbay_amazonus_encryption_iv')); + } + + public function __get($name) { + return $this->registry->get($name); + } + + public function getEncryptionKey() { + return $this->encryption_key; + } + + public function setEncryptionKey($key) { + $this->encryption_key = $key; + } + + public function getEncryptionIv() { + return $this->encryption_iv; + } + + public function setEncryptionIv($encryption_iv) { + $this->encryption_iv = $encryption_iv; + } + + public function call($method, $data = array(), $use_json = true) { + if (!empty($data)) { + if ($use_json) { + $string = json_encode($data); + } else { + $string = $data; + } + + $encrypted = $this->openbay->encrypt($string, $this->getEncryptionKey(), $this->getEncryptionIv(), false); + } else { + $encrypted = ''; + } + + $post_data = array( + 'token' => $this->token, + 'data' => base64_encode($encrypted), + 'opencart_version' => VERSION + ); + + $headers = array(); + $headers[] = 'X-Endpoint-Version: 2'; + + $defaults = array( + CURLOPT_HEADER => 0, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POST => 1, + CURLOPT_URL => $this->url . $method, + CURLOPT_USERAGENT => 'OpenBay Pro for Amazonus/Opencart', + CURLOPT_FRESH_CONNECT => 1, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FORBID_REUSE => 1, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_POSTFIELDS => http_build_query($post_data, '', "&"), + ); + + $curl = curl_init(); + + curl_setopt_array($curl, $defaults); + + $response = curl_exec($curl); + + curl_close($curl); + + return $response; + } + + public function callNoResponse($method, $data = array(), $use_json = true) { + if (!empty($data)) { + if ($use_json) { + $string = json_encode($data); + } else { + $string = $data; + } + + $encrypted = $this->openbay->encrypt($string, $this->getEncryptionKey(), $this->getEncryptionIv(), false); + } else { + $encrypted = ''; + } + + $post_data = array( + 'token' => $this->token, + 'data' => base64_encode($encrypted), + 'opencart_version' => VERSION + ); + + $headers = array(); + $headers[] = 'X-Endpoint-Version: 2'; + + $defaults = array( + CURLOPT_HEADER => 0, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POST => 1, + CURLOPT_URL => $this->url . $method, + CURLOPT_USERAGENT => 'OpenBay Pro for Amazonus/Opencart', + CURLOPT_FRESH_CONNECT => 1, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FORBID_REUSE => 1, + CURLOPT_TIMEOUT => 2, + CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_POSTFIELDS => http_build_query($post_data, '', "&"), + ); + $curl = curl_init(); + + curl_setopt_array($curl, $defaults); + + curl_exec($curl); + + curl_close($curl); + } + + public function getServer() { + return $this->url; + } + + public function productUpdateListen($product_id, $data = array()) { + $logger = new \Log('amazonus_stocks.log'); + $logger->write('productUpdateListen(), product ID: ' . $product_id); + + $product = $this->db->query("SELECT DISTINCT * FROM `" . DB_PREFIX . "product` WHERE `product_id` = '" . (int)$product_id . "' LIMIT 1")->row; + + if ($this->openbay->addonLoad('openstock') && (isset($product['has_option']) && $product['has_option'] == 1)) { + $this->load->model('extension/module/openstock'); + $logger->write('Variant item'); + + $quantity_data = array(); + + // check if post data['variant'], if not then call db to get variants + if (!isset($data['variant'])) { + $variants = $this->model_extension_module_openstock->getVariants($product_id); + } else { + $variants = $data['variant']; + } + + foreach ($variants as $variant) { + $amazon_sku_rows = $this->db->query("SELECT `amazonus_sku` FROM `" . DB_PREFIX . "amazonus_product_link` WHERE `product_id` = '" . (int)$product_id . "' AND `var` = '" . $this->db->escape($variant['sku']) . "'")->rows; + + foreach($amazon_sku_rows as $amazon_sku_row) { + $quantity_data[$amazon_sku_row['amazonus_sku']] = $variant['stock']; + } + } + + if(!empty($quantity_data)) { + $logger->write('Updating with: ' . print_r($quantity_data, true)); + $this->updateQuantities($quantity_data); + } else { + $logger->write('Not required.'); + } + } else { + $this->putStockUpdateBulk(array($product_id)); + } + + $logger->write('productUpdateListen() - finished'); + } + + public function bulkUpdateOrders($orders) { + // Is the module enabled and called from admin? + if ($this->config->get('openbay_amazonus_status') != 1 || !defined('HTTPS_CATALOG')) { + return; + } + $this->load->model('extension/openbay/amazonus'); + + $log = new \Log('amazonus.log'); + $log->write('Called bulkUpdateOrders method'); + + $request = array( + 'orders' => array(), + ); + + foreach ($orders as $order) { + $amazon_order = $this->getOrder($order['order_id']); + $amazon_order_products = $this->model_extension_openbay_amazonus->getAmazonusOrderedProducts($order['order_id']); + + $products = array(); + + foreach ($amazon_order_products as $amazon_order_product) { + $products[] = array( + 'amazon_order_item_id' => $amazon_order_product['amazonus_order_item_id'], + 'quantity' => $amazon_order_product['quantity'], + ); + } + + $order_info = array( + 'amazon_order_id' => $amazon_order['amazonus_order_id'], + 'status' => $order['status'], + 'products' => $products, + ); + + if ($order['status'] == 'shipped' && !empty($order['carrier'])) { + if ($order['carrier_from_list']) { + $order_info['carrier_id'] = $order['carrier']; + } else { + $order_info['carrier_name'] = $order['carrier']; + } + + $order_info['tracking'] = $order['tracking']; + } + + $request['orders'][] = $order_info; + } + + $log->write('order/bulkUpdate call: ' . print_r($request, 1)); + + $response = $this->call('order/bulkUpdate', $request); + + $log->write('order/bulkUpdate response: ' . $response); + } + + public function updateOrder($order_id, $order_status_string, $courier_id = '', $courier_from_list = true, $tracking_no = '') { + + if ($this->config->get('openbay_amazonus_status') != 1) { + return; + } + + /* Is called from admin? */ + if (!defined('HTTPS_CATALOG')) { + return; + } + + $amazonus_order = $this->getOrder($order_id); + + if(!$amazonus_order) { + return; + } + + $amazonus_order_id = $amazonus_order['amazonus_order_id']; + + $log = new \Log('amazonus.log'); + $log->write("Order's $amazonus_order_id status changed to $order_status_string"); + + $this->load->model('extension/openbay/amazonus'); + $amazonus_order_products = $this->model_extension_openbay_amazonus->getAmazonusOrderedProducts($order_id); + + $request_node = new \SimpleXMLElement('<Request/>'); + + $request_node->addChild('AmazonusOrderId', $amazonus_order_id); + $request_node->addChild('Status', $order_status_string); + + if(!empty($courier_id)) { + if($courier_from_list) { + $request_node->addChild('CourierId', $courier_id); + } else { + $request_node->addChild('CourierOther', $courier_id); + } + $request_node->addChild('TrackingNo', $tracking_no); + } + + $order_items_node = $request_node->addChild('OrderItems'); + + foreach ($amazonus_order_products as $product) { + $new_order_item = $order_items_node->addChild('OrderItem'); + $new_order_item->addChild('ItemId', htmlspecialchars($product['amazonus_order_item_id'])); + $new_order_item->addChild('Quantity', (int)$product['quantity']); + } + + $doc = new \DOMDocument('1.0'); + $doc->preserveWhiteSpace = false; + $doc->loadXML($request_node->asXML()); + $doc->formatOutput = true; + + $this->model_extension_openbay_amazonus->updateAmazonusOrderTracking($order_id, $courier_id, $courier_from_list, !empty($courier_id) ? $tracking_no : ''); + $log->write('Request: ' . $doc->saveXML()); + $response = $this->call('order/update2', $doc->saveXML(), false); + $log->write("Response for Order's status update: $response"); + } + + public function getCategoryTemplates() { + $result = $this->call("productv2/RequestTemplateList", array('list' => true)); + if(isset($result)) { + return (array)json_decode($result); + } else { + return array(); + } + } + + public function registerInsertion($data) { + $result = $this->call("productv2/RegisterInsertionRequest", $data); + if(isset($result)) { + return (array)json_decode($result); + } else { + return array(); + } + } + + public function insertProduct($data) { + $result = $this->call("productv2/InsertProductRequest", $data); + if(isset($result)) { + return (array)json_decode($result); + } else { + return array(); + } + } + + public function updateQuantities($data) { + $result = $this->call("product/UpdateQuantityRequest", $data); + if(isset($result)) { + return (array)json_decode($result); + } else { + return array(); + } + } + + public function getStockUpdatesStatus($data) { + $result = $this->call("status/StockUpdates", $data); + if(isset($result)) { + return $result; + } else { + return false; + } + } + + public function putStockUpdateBulk($product_id_array, $end_inactive = false){ + $logger = new \Log('amazonus_stocks.log'); + $logger->write('putStockUpdateBulk(), End inactive: ' . $end_inactive . ', ids: ' . json_encode($product_id_array)); + + $quantity_data = array(); + + foreach($product_id_array as $product_id) { + $linked_skus = $this->db->query("SELECT `amazonus_sku` FROM `" . DB_PREFIX . "amazonus_product_link` WHERE `product_id` = '" . (int)$product_id . "'")->rows; + + if (!empty($linked_skus)) { + foreach($linked_skus as $sku) { + $product = $this->db->query("SELECT quantity, status FROM `" . DB_PREFIX . "product` WHERE `product_id` = '" . (int)$product_id . "'")->row; + + if(!empty($product)) { + if($end_inactive && $product['status'] == '0') { + $quantity_data[$sku['amazonus_sku']] = 0; + } else { + $quantity_data[$sku['amazonus_sku']] = $product['quantity']; + } + } + } + } else { + $logger->write('No linked SKU'); + } + } + + if(!empty($quantity_data)) { + $logger->write('New Qty:' . print_r($quantity_data, true)); + + $response = $this->updateQuantities($quantity_data); + + $logger->write('API Response: ' . print_r($response, true)); + } else { + $logger->write('No update needed'); + } + } + + public function getOrderdProducts($order_id) { + return $this->db->query("SELECT `op`.`product_id`, `p`.`quantity` as `quantity_left` FROM `" . DB_PREFIX . "order_product` as `op` LEFT JOIN `" . DB_PREFIX . "product` as `p` ON `p`.`product_id` = `op`.`product_id` WHERE `op`.`order_id` = '" . (int)$order_id . "'")->rows; + } + + public function validate() { + if ($this->config->get('openbay_amazonus_status') != 0 && + $this->config->get('openbay_amazonus_token') != '' && + $this->config->get('openbay_amazonus_encryption_key') != '' && + $this->config->get('openbay_amazonus_encryption_iv') != '') { + return true; + } else { + return false; + } + } + + public function deleteProduct($product_id){ + $this->db->query("DELETE FROM `" . DB_PREFIX . "amazonus_product_link` WHERE `product_id` = '" . (int)$product_id . "'"); + } + + public function orderDelete($order_id){ + /** + * @todo + */ + } + + public function getOrder($order_id) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "amazonus_order` WHERE `order_id` = '" . (int)$order_id . "' LIMIT 1"); + + if ($qry->num_rows > 0) { + return $qry->row; + } else { + return false; + } + } + + public function getCarriers() { + return array( + "Blue Package", + "Canada Post", + "City Link", + "DHL", + "DHL Global Mail", + "Fastway", + "FedEx", + "FedEx SmartPost", + "GLS", + "GO!", + "Hermes Logistik Gruppe", + "Newgistics", + "NipponExpress", + "OSM", + "OnTrac", + "Parcelforce", + "Royal Mail", + "SagawaExpress", + "Streamlite", + "TNT", + "Target", + "UPS", + "UPS Mail Innovations", + "USPS", + "YamatoTransport", + ); + } + + public function parseCategoryTemplate($xml) { + $simplexml = null; + + libxml_use_internal_errors(true); + if(($simplexml = simplexml_load_string($xml)) == false) { + return false; + } + + $category = (string)$simplexml->filename; + + $tabs = array(); + foreach($simplexml->tabs->tab as $tab) { + $attributes = $tab->attributes(); + $tabs[] = array( + 'id' => (string)$attributes['id'], + 'name' => (string)$tab->name, + ); + } + + $fields = array(); + $field_types = array('required', 'desired', 'optional'); + foreach ($field_types as $type) { + foreach ($simplexml->fields->$type->field as $field) { + $attributes = $field->attributes(); + $fields[] = array( + 'name' => (string)$attributes['name'], + 'title' => (string)$field->title, + 'definition' => (string)$field->definition, + 'accepted' => (array)$field->accepted, + 'type' => (string)$type, + 'child' => false, + 'order' => (isset($attributes['order'])) ? (string)$attributes['order'] : '', + 'tab' => (string)$attributes['tab'], + ); + } + foreach ($simplexml->fields->$type->childfield as $field) { + $attributes = $field->attributes(); + $fields[] = array( + 'name' => (string)$attributes['name'], + 'title' => (string)$field->title, + 'definition' => (string)$field->definition, + 'accepted' => (array)$field->accepted, + 'type' => (string)$type, + 'child' => true, + 'parent' => (array)$field->parent, + 'order' => (isset($attributes['order'])) ? (string)$attributes['order'] : '', + 'tab' => (string)$attributes['tab'], + ); + } + } + + foreach($fields as $index => $field) { + $fields[$index]['unordered_index'] = $index; + } + + usort($fields, array('openbay\Amazonus','compareFields')); + + return array( + 'category' => $category, + 'fields' => $fields, + 'tabs' => $tabs, + ); + } + + private static function compareFields($field1, $field2) { + if($field1['order'] == $field2['order']) { + return ($field1['unordered_index'] < $field2['unordered_index']) ? -1 : 1; + } else if(!empty($field1['order']) && empty($field2['order'])) { + return -1; + } else if(!empty($field2['order']) && empty($field1['order'])) { + return 1; + } else { + return ($field1['order'] < $field2['order']) ? -1 : 1; + } + } +} diff --git a/public/system/library/openbay/ebay.php b/public/system/library/openbay/ebay.php new file mode 100644 index 0000000..f94d48d --- /dev/null +++ b/public/system/library/openbay/ebay.php @@ -0,0 +1,1514 @@ +<?php +namespace openbay; + +final class Ebay { + private $token; + private $encryption_key; + private $encryption_iv; + private $url = 'https://uk.openbaypro.com/'; + private $registry; + private $no_log = array('notification/getPublicNotifications/', 'setup/getEbayCategories/', 'item/getItemAllList/', 'account/validate/', 'item/getItemListLimited/'); + private $logger; + private $max_log_size = 50; //max log size in Mb + + public function __construct($registry) { + $this->registry = $registry; + $this->token = $this->config->get('ebay_token'); + $this->secret = $this->config->get('ebay_secret'); + $this->logging = $this->config->get('ebay_logging'); + $this->tax = $this->config->get('ebay_tax'); + $this->server = 1; + $this->lasterror = ''; + $this->lastmsg = ''; + + if ($this->logging == 1) { + $this->setLogger(); + } + + $this->setEncryptionKey($this->config->get('ebay_encryption_key')); + $this->setEncryptionIv($this->config->get('ebay_encryption_iv')); + } + + public function __get($name) { + return $this->registry->get($name); + } + + public function getEncryptionKey() { + return $this->encryption_key; + } + + public function setEncryptionKey($key) { + $this->encryption_key = $key; + } + + public function getEncryptionIv() { + return $this->encryption_iv; + } + + public function setEncryptionIv($encryption_iv) { + $this->encryption_iv = $encryption_iv; + } + + public function call($call, array $post = null, array $options = array(), $content_type = 'json', $status_override = false) { + if ($this->config->get('ebay_status') == 1 || $status_override == true) { + $this->lasterror = ''; + $this->lastmsg = ''; + + if (!in_array($call, $this->no_log)) { + $this->log('call(' . $call . ') - Data: ' . json_encode($post)); + } + + if (defined("HTTPS_CATALOG")) { + $domain = HTTPS_CATALOG; + } else { + $domain = $this->config->get('config_url'); + } + + $headers = array(); + $headers[] = 'X-Endpoint-Version: 2'; + + $data = array('token' => $this->token, 'secret' => $this->secret, 'server' => $this->server, 'domain' => $domain, 'openbay_version' => (int)$this->config->get('feed_openbaypro_version'), 'opencart_version' => VERSION, 'data' => $post, 'content_type' => $content_type, 'language' => $this->config->get('feed_openbaypro_language')); + + $defaults = array( + CURLOPT_HEADER => 0, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POST => 1, + CURLOPT_URL => $this->url . $call, + CURLOPT_USERAGENT => "OpenBay Pro for eBay/OpenCart", + CURLOPT_FRESH_CONNECT => 1, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FORBID_REUSE => 1, + CURLOPT_TIMEOUT => 0, + CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_POSTFIELDS => http_build_query($data, '', "&") + ); + + $curl = curl_init(); + curl_setopt_array($curl, ($options + $defaults)); + if (! $result = curl_exec($curl)) { + $this->log('call() - Curl Failed ' . curl_error($curl) . ' ' . curl_errno($curl)); + } + curl_close($curl); + + if (!in_array($call, $this->no_log)) { + $this->log('call() - Result of : "' . $result . '"'); + } + + if ($content_type == 'json') { + $encoding = mb_detect_encoding($result); + + if ($encoding == 'UTF-8') { + $result = preg_replace('/[^(\x20-\x7F)]*/', '', $result); + } + + $result = json_decode($result, 1); + $this->lasterror = $result['error']; + $this->lastmsg = $result['msg']; + + if (!empty($result['data'])) { + return $result['data']; + } else { + return false; + } + }elseif ($content_type == 'xml') { + $result = simplexml_load_string($result); + $this->lasterror = $result->error; + $this->lastmsg = $result->msg; + + if (!empty($result->data)) { + return $result->data; + } else { + return false; + } + } + } else { + $this->log('call() - OpenBay Pro not active'); + } + } + + public function callNoResponse($call, array $post = null, array $options = array(), $content_type = 'json') { + if ($this->config->get('ebay_status') == 1) { + $this->log('openbay_noresponse_call(' . $call . ') - Data :' . json_encode($post)); + + if (defined("HTTPS_CATALOG")) { + $domain = HTTPS_CATALOG; + } else { + $domain = $this->config->get('config_url'); + } + + $headers = array(); + $headers[] = 'X-Endpoint-Version: 2'; + + $data = array('token' => $this->token, 'secret' => $this->secret, 'server' => $this->server, 'domain' => $domain, 'openbay_version' => (int)$this->config->get('feed_openbaypro_version'), 'opencart_version' => VERSION, 'data' => $post, 'content_type' => $content_type, 'language' => $this->config->get('feed_openbaypro_language')); + + $defaults = array( + CURLOPT_HEADER => 0, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POST => 1, + CURLOPT_URL => $this->url . $call, + CURLOPT_USERAGENT => "OpenBay Pro for eBay/OpenCart", + CURLOPT_FRESH_CONNECT => 1, + CURLOPT_RETURNTRANSFER => 0, + CURLOPT_FORBID_REUSE => 1, + CURLOPT_TIMEOUT => 5, + CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_POSTFIELDS => http_build_query($data, '', "&") + ); + + $curl = curl_init(); + curl_setopt_array($curl, ($options + $defaults)); + curl_exec($curl); + $this->log(curl_error($curl)); + curl_close($curl); + } else { + $this->log('openbay_noresponse_call() - OpenBay Pro not active . '); + } + } + + private function setLogger() { + if(file_exists(DIR_LOGS . 'ebaylog.log')) { + if(filesize(DIR_LOGS . 'ebaylog.log') > ($this->max_log_size * 1000000)) { + rename(DIR_LOGS . 'ebaylog.log', DIR_LOGS . '_ebaylog_' . date('Y-m-d_H-i-s') . '.log'); + } + } + + $this->logger = new \Log('ebaylog.log'); + } + + public function log($data, $write = true) { + if ($this->logging == 1) { + if (function_exists('getmypid')) { + $process_id = getmypid(); + $data = $process_id . ' - ' . print_r($data, true); + } + + $this->logger->write($data); + } + } + + public function getServer() { + return $this->url; + } + + public function getSetting($key) { + $qry = $this->db->query("SELECT `data` FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = '" . $this->db->escape($key) . "' LIMIT 1"); + + if ($qry->num_rows > 0) { + return unserialize($qry->row['data']); + } else { + return false; + } + } + + public function getEbayItemId($product_id) { + $this->log('getEbayItemId() - Product ID: ' . $product_id); + + $qry = $this->db->query("SELECT `ebay_item_id` FROM `" . DB_PREFIX . "ebay_listing` WHERE `product_id` = '" . (int)$product_id . "' AND `status` = '1' LIMIT 1"); + + if (!$qry->num_rows) { + $this->log('No link found - getEbayItemId()'); + return false; + } else { + $this->log('Returning ' . $qry->row['ebay_item_id'] . ' - getEbayItemId()'); + return $qry->row['ebay_item_id']; + } + } + + public function getEndedEbayItemId($product_id) { + $this->log('getEndedEbayItemId() - ID: ' . $product_id); + + $qry = $this->db->query("SELECT `ebay_item_id` FROM `" . DB_PREFIX . "ebay_listing` WHERE `product_id` = '" . (int)$product_id . "' AND `status` = '0' ORDER BY `ebay_listing_id` DESC LIMIT 1"); + + if (!$qry->num_rows) { + $this->log('getEndedEbayItemId() - No link'); + return false; + } else { + $this->log('getEndedEbayItemId() - Returning ' . $qry->row['ebay_item_id']); + return $qry->row['ebay_item_id']; + } + } + + public function removeItemByItemId($item_id) { + $this->log('removeItemByItemId() - ID: ' . $item_id); + + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_listing` SET `status` = '0' WHERE `ebay_item_id` = '" . $this->db->escape($item_id) . "'"); + + $this->db->query("DELETE FROM `" . DB_PREFIX . "ebay_stock_reserve` WHERE `item_id` = '" . $this->db->escape($item_id) . "'"); + } + + public function removeItemByProductId($product_id) { + $this->log('removeItemByProductId() - ID: ' . $product_id . ''); + + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_listing` SET `status` = '0' WHERE `product_id` = '" . (int)$product_id . "'"); + + $this->db->query("DELETE FROM `" . DB_PREFIX . "ebay_stock_reserve` WHERE `product_id` = '" . (int)$product_id . "'"); + } + + public function deleteProduct($product_id) { + $this->log('deleteProduct() - ID: ' . $product_id); + + $this->db->query("DELETE FROM `" . DB_PREFIX . "ebay_listing` WHERE `product_id` = '" . (int)$product_id . "'"); + $this->db->query("DELETE FROM `" . DB_PREFIX . "ebay_stock_reserve` WHERE `product_id` = '" . (int)$product_id . "'"); + } + + public function orderDelete($order_id) { + /** + * @todo + */ + } + + public function getLiveListingArray() { + /* + * Returns the list of linked items with eBay from the database + * @return array ([product id] = ebay item id) + */ + $this->log('getLiveListingArray()'); + + $qry = $this->db->query("SELECT `product_id`, `ebay_item_id` FROM `" . DB_PREFIX . "ebay_listing` WHERE `status` = '1'"); + + $data = array(); + if ($qry->num_rows > 0) { + foreach ($qry->rows as $row) { + $data[$row['product_id']] = $row['ebay_item_id']; + } + } + + return $data; + } + + public function getEndedListingArray() { + $this->log('getEndedListingArray()'); + $active = $this->getLiveListingArray(); + + $qry = $this->db->query("SELECT e.* FROM (SELECT `product_id`, MAX(`ebay_listing_id`) as `ebay_listing_id` FROM `" . DB_PREFIX . "ebay_listing` WHERE `status` = 0 GROUP BY `product_id`) `a` INNER JOIN `" . DB_PREFIX . "ebay_listing` `e` ON (`e`.`ebay_listing_id` = `a`.`ebay_listing_id`)"); + + $data = array(); + if ($qry->num_rows > 0) { + foreach ($qry->rows as $row) { + $data[$row['product_id']] = $row['ebay_item_id']; + } + } + + foreach ($active as $k => $v) { + if (array_key_exists($k, $data)) { + unset($data[$k]); + } + } + + return $data; + } + + public function getLiveProductArray() { + /** + * Returns the list of linked items with eBay from the database + * @return array ([ebay item id] = product id) + */ + $qry = $this->db->query("SELECT `product_id`, `ebay_item_id` FROM `" . DB_PREFIX . "ebay_listing` WHERE `status` = '1'"); + + $data = array(); + if ($qry->num_rows) { + foreach ($qry->rows as $row) { + $data[$row['ebay_item_id']] = $row['product_id']; + } + } + + return $data; + } + + public function endItem($item_id) { + $this->log('endItem() - ID "' . $item_id); + + if ($this->config->get('ebay_enditems') == 1) { + $this->call('item/endItem/', array('id' => $item_id)); + $this->removeItemByItemId($item_id); + + if ($this->lasterror != true) { + $this->log('endItem() - OK'); + return array('error' => false, 'msg' => 'ok'); + } else { + return array('error' => true, 'msg' => $this->lasterror); + } + } else { + $this->removeItemByItemId($item_id); + $this->log('endItem() - config has disabled ending items'); + + $message = "An item has gone out of stock but your settings are not set to end eBay items automatically.\r\n\r\n"; + $message.= "You need to ensure you have stock left of this item or end your eBay listing manually.\r\n\r\n"; + $message.= "eBay item ID: $item_id"; + + $this->notifyAdmin('eBay item not ended: ' . $item_id, $message); + + return array('error' => true, 'msg' => 'Settings do not allow you to end items, but the link has been removed . '); + } + } + + public function ebaySaleStockReduce($product_id, $sku = null) { + /** + * Gets the product info from an ID and sends to ebay update method. + */ + $this->log('ebaySaleStockReduce() - Is stock update needed (Item ID: ' . $product_id . ',SKU: ' . $sku . ')'); + + if (!empty($product_id)) { + if ($sku == null) { + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product` WHERE `product_id` = '" . (int)$product_id . "' LIMIT 1"); + $this->log('ebaySaleStockReduce() - Send item ID: "' . $product_id . '", Stock: "' . $query->row['quantity'] . '" to decideEbayStockAction()'); + $this->decideEbayStockAction($product_id, $query->row['quantity'], $query->row['subtract']); + } else { + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_option_variant` WHERE `product_id` = '" . (int)$product_id . "' AND `sku` = '" . $this->db->escape($sku) . "' LIMIT 1"); + $this->log('ebaySaleStockReduce() - Send item ID: ' . $product_id . ', VAR: ' . $sku . ', passing ' . $query->row['stock'] . ' to decideEbayStockAction()'); + $this->decideEbayStockAction($product_id, $query->row['stock'], $query->row['subtract'], $sku); + } + } + } + + public function notifyAdmin($subject, $message) { + $this->log('Sending email to: ' . $this->config->get('config_email') . ' - notifyAdmin()'); + + $mail = new \Mail(); + $mail->protocol = $this->config->get('config_mail_protocol'); + $mail->parameter = $this->config->get('config_mail_parameter'); + $mail->smtp_hostname = $this->config->get('config_mail_smtp_hostname'); + $mail->smtp_username = $this->config->get('config_mail_smtp_username'); + $mail->smtp_password = html_entity_decode($this->config->get('config_mail_smtp_password'), ENT_QUOTES, 'UTF-8'); + $mail->smtp_port = $this->config->get('config_mail_smtp_port'); + $mail->smtp_timeout = $this->config->get('config_mail_smtp_timeout'); + + $mail->setTo($this->config->get('config_email')); + $mail->setFrom($this->config->get('config_email')); + $mail->setSender(html_entity_decode($this->config->get('config_name'), ENT_QUOTES, 'UTF-8')); + $mail->setSubject(html_entity_decode($subject, ENT_QUOTES, 'UTF-8')); + $mail->setText($message); + $mail->send(); + } + + public function validateJsonDecode($data) { + $data = (string)$data; + + $encoding = mb_detect_encoding($data); + + if ($encoding == 'UTF-8') { + $data = preg_replace('/[^(\x20-\x7F)]*/', '', $data); + $data = preg_replace('#\\\\x[0-9a-fA-F]{2,2}#', '', $data); + } + + $data = json_decode($data); + + if (function_exists('json_last_error')) { + switch (json_last_error()) { + case JSON_ERROR_NONE: + $this->log('validateJsonDecode() - No json decode errors'); + break; + case JSON_ERROR_DEPTH: + $this->log('validateJsonDecode() - Maximum stack depth exceeded'); + break; + case JSON_ERROR_STATE_MISMATCH: + $this->log('validateJsonDecode() - Underflow or the modes mismatch'); + break; + case JSON_ERROR_CTRL_CHAR: + $this->log('validateJsonDecode() - Unexpected control character found'); + break; + case JSON_ERROR_SYNTAX: + $this->log('validateJsonDecode() - Syntax error, malformed JSON'); + break; + case JSON_ERROR_UTF8: + $this->log('validateJsonDecode() - Malformed UTF-8 characters, possibly incorrectly encoded'); + break; + default: + $this->log('validateJsonDecode() - Unknown error'); + break; + } + } else { + $this->log('validateJsonDecode() - json_last_error PHP function does not exist'); + } + + return $data; + } + + private function eBayShippingStatus($item, $txn, $status, $tracking_no = '', $carrier_id = '') { + $this->log('eBayShippingStatus() - Update order shipping status (Item: ' . $item . ',Txn: ' . $txn . ',Status:' . $status . ',Tracking: ' . $tracking_no . ', Carrier: ' . $carrier_id . ')'); + return $this->call('order/shippingStatus/', array('item' => $item, 'txn' => $txn, 'status' => $status, 'carrier' => $carrier_id, 'tracking' => $tracking_no)); + } + + private function eBayPaymentStatus($item, $txn, $status) { + $this->log('eBayPaymentStatus() - Updates order payment status (Item: ' . $item . ',Txn: ' . $txn . ',Status:' . $status . ')'); + return $this->call('order/paymentStatus/', array('item' => $item, 'txn' => $txn, 'status' => $status)); + } + + private function getSaleRecord($sale_id) { + $this->log('getSaleRecord() - Get ebay sale record ID: ' . $sale_id); + return $this->call('order/getSmpRecord/', array('id' => $sale_id)); + } + + public function getEbayActiveListings() { + $this->log('getEbayActiveListings() - Get active eBay items from API'); + return $this->call('item/getItemAllList/'); + } + + public function getEbayItemList($limit = 100, $page = 1, $filter = array()) { + $this->log('getEbayItemList() - Get active eBay items from API'); + return $this->call('item/getItemListLimited/', array('page' => $page, 'limit' => $limit, 'filter' => $filter)); + } + + public function disableProduct($product_id) { + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET `status` = 0 WHERE `product_id` = '" . (int)$product_id . "' LIMIT 1"); + } + + public function disableVariant($product_id, $sku) { + $this->db->query("UPDATE `" . DB_PREFIX . "product_option_variant` SET `active` = 0 WHERE `product_id` = '" . (int)$product_id . "' AND `sku` = '" . $this->db->escape($sku) . "' LIMIT 1"); + } + + public function putStockUpdate($item_id, $stock, $sku = null) { + $this->log('putStockUpdate()'); + $this->log('putStockUpdate() - New local stock: ' . $stock); + + $listing = $this->call('item/getItem', array('itemId' => $item_id)); + $product_id = $this->getProductId($item_id); + $reserve = $this->getReserve($product_id, $item_id, (($sku != null) ? $sku : '')); + + if ($listing['status'] == 1 ) { + if ($reserve != false) { + $this->log('putStockUpdate() - Reserve stock: ' . $reserve); + + if ($stock > $reserve) { + $this->log('putStockUpdate() - Stock is larger than reserve, setting level to reserve'); + $stock = $reserve; + } + } + + if ($sku == null) { + $this->log('putStockUpdate() - Listing stock: ' . $listing['qty'] . ', new stock: ' . $stock); + + if ($stock <= 0) { + if ($this->config->get('ebay_disable_nostock') == 1) { + $this->disableProduct($product_id); + } + + $this->endItem($item_id); + return true; + }elseif ($listing['qty'] != $stock) { + $this->call('item/reviseStock/', array('itemId' => $item_id, 'stock' => $stock)); + $this->log('putStockUpdate() - OK'); + return true; + } else { + $this->log('putStockUpdate() - No update needed'); + return false; + } + } else { + // Need to loop over current item check if other variants have stock + $variant_stock = false; + foreach ($listing['variation']['vars'] as $var) { + if (($var['sku'] != $sku) && ($var['qty'] > 0)) { + //other variations have stock + $variant_stock = true; + $this->log('Another variation has stock (SKU: ' . $var['sku'] . ')'); + break; + } + } + + if ($stock <= 0) { + if ($this->config->get('ebay_disable_nostock') == 1) { + $this->disableVariant($product_id, $sku); + } + } + + if ($variant_stock == true || $stock > 0) { + $this->log('putStockUpdate() - Revising item with Item ID "' . $item_id . '" to stock level "' . $stock . '", sku "' . $sku . '"'); + $this->call('item/reviseStock/', array('itemId' => $item_id, 'stock' => $stock, 'sku' => $sku)); + return true; + } else { + $this->log('putStockUpdate() - Sending end for item, no variants have stock!'); + $this->endItem($item_id); + } + } + } else { + $this->removeItemByItemId($item_id); + + if($sku == null) { + if ($stock <= 0 && $this->config->get('ebay_disable_nostock') == 1) { + $this->disableProduct($product_id); + } + } else { + if ($stock <= 0 && $this->config->get('ebay_disable_nostock') == 1) { + $this->disableVariant($product_id, $sku); + } + } + + $this->log('putStockUpdate() - Listing not active, item id: ' . $item_id . ', status returned: ' . $listing['statusActual']); + } + } + + public function putStockUpdateBulk($product_id_array, $end_inactive = false) { + // We know is that these product ID's have been modified. They should only be passed if the stock has changed so we can assume this. + $this->log('putStockUpdateBulk()'); + + $openstock = false; + if ($this->openbay->addonLoad('openstock') == true) { + $this->load->model('extension/module/openstock'); + $openstock = true; + } + + // Get the active OpenCart items that were linked to eBay If they have stock now, relist them. + $ended_data = $this->getEndedListingArray(); + + /** + * Get the active OpenCart items that are also linked + * Compare against the stock from eBay + * If listing active and local stock = 0, end it + * If listing inactive, remove link + * If listing active and local stock not the same, update it + */ + $ebay_listings = $this->getEbayActiveListings(); + $live_data = $this->getLiveListingArray(); + + $linked_items = array(); + $linked_ended_items = array(); + + foreach ($product_id_array as $product_id) { + if (array_key_exists((int)$product_id, $live_data)) { + //product has been passed and is linked to active item + $linked_items[] = array('productId' => (int)$product_id, 'itemId' => $live_data[$product_id]); + }elseif (array_key_exists((int)$product_id, $ended_data)) { + //product has been passed and is not currently active + $linked_ended_items[] = array('productId' => (int)$product_id, 'itemId' => $ended_data[$product_id]); + } else { + //product does not exist in live or ended links so has never been linked. + } + } + + //loop through ended listings, if back in stock and not multi var - relist it + foreach ($linked_ended_items as $item) { + if ($openstock == true) { + $options = $this->model_extension_module_openstock->getVariants($item['productId']); + } else { + $options = array(); + } + + if (empty($options)) { + //get the stock level of the linked items + $local_stock = $this->getProductStockLevel($item['productId']); + + if ((int)$local_stock['quantity'] > 0 && $local_stock['status'] == 1) { + //product has stock and is enabled, so re list it. + $reserve = $this->getReserve($item['productId'], $item['itemId']); + + if ($reserve != false) { + if ($local_stock['quantity'] > $reserve) { + $local_stock['quantity'] = $reserve; + } + } + + if ($this->config->get('ebay_relistitems') == 1) { + //relist item with new stock + $this->relistItem($item['itemId'], $item['productId'], (int)$local_stock['quantity']); + } + } + } else { + $this->log('putStockUpdateBulk() - options existed for item (' . $item['itemId'] . ') when trying to relist'); + // @todo - support relisting of variant items, if possible with ebay! + } + } + + //loop through the active listings and update the store or end the item + foreach ($linked_items as $item) { + //get the stock level of the linked item + $local_stock = $this->getProductStockLevel($item['productId']); + + //check if the itemid was returned by ebay, if not unlink it as it is ended. + if (!isset($ebay_listings[$item['itemId']])) { + $this->log('eBay item was not returned, removing link (' . $item['itemId'] . ')'); + $this->removeItemByItemId($item['itemId']); + } else { + //check if the local item is now inactive - end if it is + if ($end_inactive == true && $local_stock['status'] == 0) { + $this->endItem($item['itemId']); + } else { + //get any options that are set for this product + if ($openstock == true) { + $options = $this->model_extension_module_openstock->getVariants($item['productId']); + } else { + $options = array(); + } + + if (empty($options) && empty($ebay_listings[$item['itemId']]['variants'])) { + $this->log('putStockUpdateBulk() - Item has no variants'); + + //compare to the ebay data get retrieved + if ((int)$local_stock['quantity'] != (int)$ebay_listings[$item['itemId']]['qty']) { + $reserve = $this->getReserve($item['productId'], $item['itemId']); + + if ($reserve != false) { + if ($local_stock['quantity'] > $reserve) { + $local_stock['quantity'] = $reserve; + } + } + + $this->putStockUpdate($item['itemId'], (int)$local_stock['quantity']); + } + }elseif (!empty($options) && !empty($ebay_listings[$item['itemId']]['variants'])) { + // This item has variants + $this->log('putStockUpdateBulk() - Variants found'); + + //create an index of var codes to search against + $var_ids = array(); + foreach ($options as $k => $v) { + $var_ids[$k] = $v['var']; + } + + //loop over eBay variants + foreach ($ebay_listings[$item['itemId']]['variants'] as $ebay_variant) { + $this->log('Checking eBay SKU: ' . $ebay_variant['sku'] . ' for item: ' . $item['itemId']); + + if (in_array($ebay_variant['sku'], $var_ids)) { + $option_id = array_search($ebay_variant['sku'], $var_ids); + + //compare the stock - if different trigger update + if ($ebay_variant['qty'] != $options[$option_id]['stock']) { + $reserve = $this->getReserve($item['productId'], $item['itemId'], $ebay_variant['sku']); + + if ($reserve != false) { + if ($options[$option_id]['stock'] > $reserve) { + $options[$option_id]['stock'] = $reserve; + } + } + + $this->log('putStockUpdateBulk() - Revising variant item: ' . $item['itemId'] . ',Stock: ' . $options[$option_id]['stock'] . ', SKU ' . $ebay_variant['sku']); + $this->call('item/reviseStock/', array('itemId' => $item['itemId'], 'stock' => $options[$option_id]['stock'], 'sku' => $ebay_variant['sku'])); + } + } + } + } else { + $this->log('Unsure if this item has variants, debug:'); + $this->log('Local: ' . $options); + $this->log('eBay: ' . serialize($ebay_listings[$item['itemId']]['variants'])); + } + } + } + } + } + + public function getProductStockLevel($product_id, $sku = '') { + $this->log('getProductStockLevel() - ID: ' . $product_id . ', SKU: ' . $sku); + + if ($sku == '' || $sku == null) { + $qry = $this->db->query("SELECT `quantity`, `status` FROM `" . DB_PREFIX . "product` WHERE `product_id` = '" . (int)$product_id . "' LIMIT 1"); + + return array('quantity' => (int)$qry->row['quantity'], 'status' => ($qry->row['status'])); + } else { + $qry = $this->db->query("SELECT `stock`, `active` FROM `" . DB_PREFIX . "product_option_variant` WHERE `product_id` = '" . (int)$product_id . "' AND `sku` = '" . $this->db->escape($sku) . "' LIMIT 1"); + + return array('quantity' => (int)$qry->row['stock'], 'status' => ($qry->row['active'])); + } + } + + public function productUpdateListen($product_id, $data = array()) { + $this->log('productUpdateListen(' . $product_id . ')'); + + $item_id = $this->getEbayItemId($product_id); + + $product = $this->db->query("SELECT DISTINCT * FROM `" . DB_PREFIX . "product` WHERE `product_id` = '" . (int)$product_id . "' LIMIT 1")->row; + + if ($item_id != false) { + $this->log('productUpdateListen(' . $product_id . ') - listing found (' . $item_id . ')'); + + if ($this->openbay->addonLoad('openstock') && (isset($product['has_option']) && $product['has_option'] == 1)) { + $this->load->model('extension/module/openstock'); + $this->load->model('tool/image'); + $this->load->model('catalog/product'); + + $this->log('productUpdateListen(' . $product_id . ') - Variant'); + + if (!isset($data['variant'])) { + $variants = $this->model_extension_module_openstock->getVariants($product_id); + } else { + $variants = $data['variant']; + } + + $variant_data = array(); + + $groups = $this->openbay->getProductOptions($product_id); + $variant_data['groups'] = array(); + $variant_data['related'] = array(); + + foreach ($groups as $grp) { + $t_tmp = array(); + foreach ($grp['product_option_value'] as $grp_node) { + $t_tmp[$grp_node['option_value_id']] = $grp_node['name']; + + $variant_data['related'][$grp_node['product_option_value_id']] = $grp['name']; + } + $variant_data['groups'][] = array('name' => $grp['name'], 'child' => $t_tmp); + } + + $v = 0; + $stock = false; + + foreach ($variants as $option) { + if (!empty($option['sku'])) { + if ($option['stock'] > 0 || $stock == true) { + $stock = true; + } + + // PRODUCT RESERVE LEVELS FOR VARIANT ITEMS (DOES NOT PASS THROUGH NORMAL SYSTEM) + $reserve = $this->getReserve($product_id, $item_id, $option['sku']); + if ($reserve != false) { + $this->log('productUpdateListen() / Variant (' . $option['sku'] . ') - Reserve stock: ' . $reserve); + + if ($option['stock'] > $reserve) { + $this->log('putStockUpdate() - Stock (' . $option['stock'] . ') is larger than reserve (' . $reserve . '), setting level to reserve'); + $option['stock'] = $reserve; + } + } + + $variant_data['opt'][$v]['sku'] = $option['sku']; + $variant_data['opt'][$v]['qty'] = $option['stock']; + $variant_data['opt'][$v]['active'] = 0; + + if ($option['active'] == 1) { + $variant_data['opt'][$v]['active'] = 1; + } + + $v++; + } + } + + $variant_data['groups'] = base64_encode(serialize($variant_data['groups'])); + $variant_data['related'] = base64_encode(serialize($variant_data['related'])); + $variant_data['id'] = $item_id; + + //send to the api to process + if (!empty($variant_data['opt'])) { + if ($stock == true) { + $this->log('productUpdateListen() - Sending to API'); + $response = $this->call('item/reviseStockVariants', $variant_data); + + return $response; + } else { + $this->log('productUpdateListen() - Ending item'); + $this->endItem($item_id); + } + } + } else { + $this->log('productUpdateListen(' . $product_id . ') - Not a variant'); + + $this->decideEbayStockAction($product_id, $product['quantity'], $product['subtract']); + + return array('msg' => 'ok', 'error' => false); + } + } else { + $old_item_id = $this->getEndedEbayItemId($product_id); + $this->log('productUpdateListen(' . $product_id . ') - Standard item. Old ID: ' . $old_item_id); + + if ($old_item_id != false) { + if ($this->openbay->addonLoad('openstock') && (isset($product['has_option']) && $product['has_option'] == 1)) { + $this->log('productUpdateListen(' . $product_id . ') - multi variant items relist not supported'); + /** + * reserved for multi variant products can be relisted automatically. + */ + } else { + $this->log('productUpdateListen(' . $product_id . ') - Normal, stock(' . $product['quantity'] . ') > 0'); + if ($product['quantity'] > 0) { + if ($this->config->get('ebay_relistitems') == 1) { + $this->relistItem($old_item_id, $product_id, $product['quantity']); + } + } + } + } else { + $this->log('productUpdateListen() - stoping, nothing found'); + } + } + } + + public function orderStatusListen($order_id, $status_id, $data = array()) { + $ebay_order = $this->getOrder($order_id); + + if (isset($ebay_order['smp_id'])) { + $ebay_id = $ebay_order['smp_id']; + } else { + $ebay_id = false; + } + + $this->log('orderStatusListen() - Order ' . $order_id . ' changed status'); + + if ($ebay_id != false) { + $this->log('orderStatusListen() - It is an eBay order, new status: ' . $status_id); + + $item_txn_array = $this->getSaleRecord($ebay_id); + + if (!empty($item_txn_array)) { + //Has it been marked as paid? + if ($status_id == $this->config->get('ebay_status_paid_id')) { + $this->log('orderStatusListen() - Updating to paid status'); + foreach ($item_txn_array as $item) { + $tmp = simplexml_load_string($this->eBayPaymentStatus($item['item'], $item['txn'], true)); + } + } + + // Has it been marked as shipped? + if ($status_id == $this->config->get('ebay_status_shipped_id')) { + $this->log('orderStatusListen() - Updating to shipped status'); + foreach ($item_txn_array as $item) { + $tmp = simplexml_load_string($this->eBayShippingStatus($item['item'], $item['txn'], true, (isset($data['tracking_no']) ? $data['tracking_no'] : ''), (isset($data['carrier_id']) ? $data['carrier_id'] : ''))); + } + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_order` SET `carrier_id` = '" . $this->db->escape((isset($data['carrier_id']) ? $data['carrier_id'] : '')) . "', `tracking_no` = '" . $this->db->escape((isset($data['tracking_no']) ? $data['tracking_no'] : '')) . "' WHERE `order_id` = '" . (int)$order_id . "' LIMIT 1"); + } + + //Has it been marked as cancelled? + if ($status_id == $this->config->get('ebay_status_cancelled_id')) { + $this->log('orderStatusListen() - Updating to cancelled status'); + foreach ($item_txn_array as $item) { + $tmp = simplexml_load_string($this->eBayPaymentStatus($item['item'], $item['txn'], false)); + } + + foreach ($item_txn_array as $item) { + $tmp = simplexml_load_string($this->eBayShippingStatus($item['item'], $item['txn'], false)); + } + } + + //Has it been marked as refunded? + if ($status_id == $this->config->get('ebay_status_refunded_id')) { + $this->log('orderStatusListen() - Updating to refunded status'); + foreach ($item_txn_array as $item) { + $tmp = simplexml_load_string($this->eBayPaymentStatus($item['item'], $item['txn'], false)); + } + + foreach ($item_txn_array as $item) { + $tmp = simplexml_load_string($this->eBayShippingStatus($item['item'], $item['txn'], false)); + } + } + } else { + // @todo return error to use here + $this->log('orderStatusListen() - The TXN array was empty, could not get order info to update status. '); + } + } else { + $this->log('orderStatusListen() - It is not an eBay order'); + } + } + + public function decideEbayStockAction($product_id, $qty, $subtract, $sku = null) { + if ($subtract == 1) { + $this->log('decideEbayStockAction() - Product ID: ' . $product_id . ', Current stock: ' . $qty); + + $item_id = $this->getEbayItemId($product_id); + + if ($item_id != false) { + $this->putStockUpdate($item_id, $qty, $sku); + } + } else { + $this->log('decideEbayStockAction() - Product ID: ' . $product_id . ' does not subtract stock'); + } + } + + public function getProductId($ebay_item, $status = 0) { + $this->log('getProductId() - Item: ' . $ebay_item); + + $status_sql = ''; + if ($status == 1) { + $status_sql = ' AND `status` = 1'; + } + + $qry = $this->db->query("SELECT `product_id` FROM `" . DB_PREFIX . "ebay_listing` WHERE `ebay_item_id` = '" . $this->db->escape($ebay_item) . "'" . $status_sql . " ORDER BY `status` DESC, `ebay_listing_id` DESC LIMIT 1"); + + if (!$qry->num_rows) { + return false; + } else { + return $qry->row['product_id']; + } + } + + public function getProductIdFromKey($key) { + $qry = $this->db->query("SELECT `product_id` FROM `" . DB_PREFIX . "ebay_listing_pending` WHERE `key` = '" . $this->db->escape($key) . "' LIMIT 1"); + + if (!$qry->num_rows) { + return false; + } else { + return $qry->row['product_id']; + } + } + + public function validate() { + if ($this->config->get('ebay_status') != 0 && $this->config->get('ebay_token') != '' && $this->config->get('ebay_secret') != '' && $this->config->get('ebay_encryption_key') != '' && $this->config->get('ebay_encryption_iv') != '') { + return true; + } else { + return false; + } + } + + public function getAllocatedStock($product_id) { + $qry = $this->db->query("SELECT SUM(`qty`) AS `total` FROM `" . DB_PREFIX . "ebay_transaction` WHERE `product_id` = '" . (int)$product_id . "' AND `order_id` = '0' LIMIT 1"); + return (int)$qry->row['total']; + } + + public function getImages() { + $this->log('getImages() - Getting product images . '); + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_image_import`"); + + if ($qry->num_rows) { + foreach ($qry->rows as $img) { + $this->log('Image: ' . $img['name']); + + $img_large = str_replace(array('$_1.JPG', '$_01.JPG', '$_12.JPG'), '$_57.JPG', $img['image_original']); + + $header_response = $this->getImageInfo($img_large); + + $copy_status = false; + + if ($header_response == 200) { + // a supersize version was found. + $copy_status = $this->getImageCopy($img_large, $img['image_new']); + } else { + // fall back to trying the original image + $header_response = $this->getImageInfo($img['image_original']); + + if ($header_response == 200) { + $copy_status = $this->getImageCopy($img['image_original'], $img['image_new']); + } + } + + if ($copy_status == true) { + if ($img['imgcount'] == 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET `image` = 'catalog/" . $img['name'] . "' WHERE `product_id` = '" . (int)$img['product_id'] . "' LIMIT 1"); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "product_image` SET `product_id` = '" . (int)$img['product_id'] . "', `image` = 'catalog/" . $this->db->escape($img['name']) . "', `sort_order` = '" . (int)$img['imgcount'] . "'"); + } + + $this->db->query("DELETE FROM `" . DB_PREFIX . "ebay_image_import` WHERE `id` = '" . (int)$img['id'] . "' LIMIT 1"); + } + } + } + } + + private function getImageInfo($url) { + $curl = curl_init($url); + curl_setopt($curl, CURLOPT_NOBODY, true); + + if(curl_exec($curl) === false) { + $this->log('Curl Error: ' . curl_error($curl)); + } + + $header_response = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + $this->log($header_response); + + curl_close($curl); + + return $header_response; + } + + private function getImageCopy($url, $image_new) { + $handle = @fopen($url, 'r'); + + if ($handle !== false) { + if (!@copy($url, $image_new)) { + $this->log('getImages() - FAILED COPY: ' . $url); + $this->log(print_r(error_get_last(), true)); + return false; + } else { + $this->log('getImages() - Copy OK : ' . $url); + return true; + } + } else { + $this->log('getImages() - URL not found : ' . $url); + return false; + } + } + + public function getEbayListing($item_id) { + $this->log('getEbayListing()'); + return $this->call('item/getItem/', array('itemId' => $item_id)); + } + + public function relistItem($item_id, $product_id, $qty) { + $this->log('relistItem() - Starting relist item, ID: ' . $item_id . ', product: ' . $product_id . ', qty: ' . $qty); + + $response = $this->call('listing/relistItem/', array('itemId' => $item_id, 'qty' => $qty)); + + if (!empty($response['ItemID'])) { + $this->log('relistItem() - Created: ' . $response['ItemID']); + $this->createLink($product_id, $response['ItemID'], ''); + return $response['ItemID']; + } else { + $this->log('relistItem() - Relisting failed ID: ' . $item_id); + return false; + } + } + + public function createLink($product_id, $item_id, $variant) { + $this->deleteProduct($product_id); + $this->removeItemByItemId($item_id); + + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_listing` SET `product_id` = '" . (int)$product_id . "', `ebay_item_id` = '" . $this->db->escape($item_id) . "', `variant` = '" . (int)$variant . "', `status` = '1'"); + } + + public function addReserve($data, $item_id, $variant) { + if ($variant == 1) { + foreach ($data['opt'] as $variation) { + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product_option_variant` WHERE `product_id` = '" . (int)$data['product_id'] . "' AND `sku` = '" . $this->db->escape($variation['sku']) . "' LIMIT 1"); + + if ($query->row['stock'] != $variation['qty']) { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_stock_reserve` SET `product_id` = '" . (int)$data['product_id'] . "', `item_id` = '" . $this->db->escape($item_id) . "', `variant_id` = '" . $this->db->escape($variation['sku']) . "', `reserve` = '" . (int)$variation['qty'] . "'"); + } + } + } else { + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product` WHERE `product_id` = '" . (int)$data['product_id'] . "' LIMIT 1"); + + if ($query->row['quantity'] != $data['qty'][0]) { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_stock_reserve` SET `product_id` = '" . (int)$data['product_id'] . "', `item_id` = '" . $this->db->escape($item_id) . "', `variant_id` = '', `reserve` = '" . (int)$data['qty'][0] . "'"); + } + } + } + + public function getReserve($product_id, $item_id, $sku = '') { + $this->log('getReserve()'); + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_stock_reserve` WHERE `product_id` = '" . (int)$product_id . "' AND `variant_id` = '" . $this->db->escape($sku) . "' AND `item_id` = '" . $this->db->escape($item_id) . "' LIMIT 1"); + + if ($query->num_rows > 0) { + $this->log('getReserve() - returning: ' . $query->row['reserve']); + return $query->row['reserve']; + } else { + $this->log('getReserve() - none'); + return false; + } + } + + public function updateReserve($product_id, $item_id, $reserve, $sku = '', $variant = 0) { + $this->log('updateReserve() - start'); + $this->log('updateReserve() - $product_id: ' . $product_id); + $this->log('updateReserve() - $item_id: ' . $item_id); + $this->log('updateReserve() - $reserve: ' . $reserve); + $this->log('updateReserve() - $sku: ' . $sku); + $this->log('updateReserve() - $variant: ' . $variant); + + if ($reserve == 0) { + $this->deleteReserve($product_id, $item_id, $sku); + } else { + if ($this->getReserve($product_id, $item_id, $sku) != false) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_stock_reserve` SET `reserve` = '" . (int)$reserve . "' WHERE `product_id` = '" . (int)$product_id . "' AND `variant_id` = '" . $this->db->escape($sku) . "' AND `item_id` = '" . $this->db->escape($item_id) . "' LIMIT 1"); + } else { + if ($variant == 0) { + $this->log('updateReserve() - not a variant'); + $this->addReserve(array('product_id' => $product_id, 'qty' => array(0 => $reserve)), $item_id, 0); + } else { + $this->log('updateReserve() - variant'); + $this->addReserve(array('product_id' => $product_id, 'opt' => array(array('sku' => $sku, 'qty' => $reserve))), $item_id, 1); + } + } + } + } + + public function deleteReserve($product_id, $item_id, $sku = '') { + $this->log('deleteReserve()'); + $this->db->query("DELETE FROM `" . DB_PREFIX . "ebay_stock_reserve` WHERE `product_id` = '" . (int)$product_id . "' AND `variant_id` = '" . $this->db->escape($sku) . "' AND `item_id` = '" . $this->db->escape($item_id) . "' LIMIT 1"); + } + + public function getCarriers() { + $qry = $this->db->query("SELECT * FROM " . DB_PREFIX . "ebay_shipping"); + + $couriers = array(); + foreach ($qry->rows as $row) { + $couriers[] = $row; + } + + return $couriers; + } + + public function getOrder($order_id) { + if ($this->openbay->testDbTable(DB_PREFIX . "ebay_order") == true) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_order` WHERE `order_id` = '" . (int)$order_id . "' LIMIT 1"); + + if ($qry->num_rows > 0) { + return $qry->row; + } else { + return false; + } + } else { + return false; + } + } + + public function getOrderBySmpId($smp_id) { + if ($this->openbay->testDbTable(DB_PREFIX . "ebay_order") == true) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_order` WHERE `smp_id` = '" . (int)$smp_id . "' LIMIT 1"); + + if ($qry->num_rows > 0) { + return $qry->row; + } else { + return false; + } + } else { + return false; + } + } + + public function updateCategories() { + $cat_array = $this->call('setup/getEbayCategories/', array(), array(), 'json', true); + + if ($this->lasterror != true) { + $this->db->query("TRUNCATE TABLE `" . DB_PREFIX . "ebay_category`"); + + if (!empty($cat_array)) { + foreach ($cat_array as $cat) { + if ($cat['BestOfferEnabled'] == true) { + $cat['BestOfferEnabled'] = 1; + } else { + $cat['BestOfferEnabled'] = 0; + } + + if ($cat['AutoPayEnabled'] == true) { + $cat['AutoPayEnabled'] = 1; + } else { + $cat['AutoPayEnabled'] = 0; + } + + $this->db->query(" + INSERT INTO `" . DB_PREFIX . "ebay_category` SET + `CategoryID` = '" . (int)$cat['CategoryID'] . "', + `CategoryParentID` = '" . (int)$cat['CategoryParentID'] . "', + `CategoryLevel` = '" . (int)$cat['CategoryLevel'] . "', + `BestOfferEnabled` = '" . (int)$cat['BestOfferEnabled'] . "', + `AutoPayEnabled` = '" . (int)$cat['AutoPayEnabled'] . "', + `CategoryName` = '" . $this->db->escape((string)$cat['CategoryName']) . "' + "); + } + } + } + + return array('msg' => $this->lastmsg, 'error' => $this->lasterror); + } + + public function updateSettings() { + $response = $this->call('setup/getEbayDetails/', array(), array(), 'json', true); + + $this->log('Getting eBay settings / sync'); + + if ($this->lasterror === false) { + if (isset($response['listing_restrictions'])) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = 'listing_restrictions' LIMIT 1"); + + if ($qry->num_rows > 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_setting_option` SET `data` = '" . $this->db->escape(serialize($response['listing_restrictions'])) . "', `last_updated` = now() WHERE `key` = 'listing_restrictions' LIMIT 1"); + $this->log('Updated listing_restrictions into ebay_setting_option table'); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_setting_option` SET `key` = 'listing_restrictions', `data` = '" . $this->db->escape(serialize($response['listing_restrictions'])) . "', `last_updated` = now()"); + $this->log('Inserted listing_restrictions into ebay_setting_option table'); + } + } else { + $this->log('listing_restrictions data not set!'); + } + + if (isset($response['product_details'])) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = 'product_details' LIMIT 1"); + + if ($qry->num_rows > 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_setting_option` SET `data` = '" . $this->db->escape(serialize($response['product_details'])) . "', `last_updated` = now() WHERE `key` = 'product_details' LIMIT 1"); + $this->log('Updated product_details into ebay_setting_option table'); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_setting_option` SET `key` = 'product_details', `data` = '" . $this->db->escape(serialize($response['product_details'])) . "', `last_updated` = now()"); + $this->log('Inserted product_details into ebay_setting_option table'); + } + } else { + $this->log('Non identifier text not set!'); + } + + if (isset($response['urls']['ViewItemURL'])) { + $this->db->query("DELETE FROM `" . DB_PREFIX . "setting` WHERE `key` = 'ebay_itm_link' LIMIT 1"); + + $this->db->query("INSERT INTO `" . DB_PREFIX . "setting` SET `value` = '" . $this->db->escape((string)$response['urls']['ViewItemURL']) . "', `key` = 'ebay_itm_link', `code` = 'openbay'"); + + $this->log('Updated eBay item link'); + } else { + $this->log('Item link URL not set!'); + } + + //ebay payment methods + if (isset($response['payment_options'])) { + $this->db->query("TRUNCATE TABLE `" . DB_PREFIX . "ebay_payment_method`"); + $this->log('Emptied ebay_payment_method table'); + + foreach ($response['payment_options'] as $child) { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_payment_method` SET `ebay_name` = '" . $this->db->escape((string)$child['PaymentOption']) . "', `local_name` = '" . $this->db->escape((string)$child['Description']) . "'"); + } + + $this->log('Populated ebay_payment_method table'); + } else { + $this->log('No payment options set!'); + } + + //ebay shipping + if (isset($response['shipping_service'])) { + $this->db->query("TRUNCATE TABLE `" . DB_PREFIX . "ebay_shipping`"); + $this->log('Emptied ebay_shipping table'); + foreach ($response['shipping_service'] as $service) { + if (!empty($service['InternationalService']) && $service['InternationalService'] == 'true') { + $service['InternationalService'] = 1; + } else { + $service['InternationalService'] = 0; + } + + if (!empty($service['ValidForSellingFlow'])) { + $service['ValidForSellingFlow'] = 1; + } else { + $service['ValidForSellingFlow'] = 0; + } + + if (!empty($service['ShippingTimeMin'])) { + $min = (int)$service['ShippingTimeMin']; + } else { + $min = 1; + } + if (!empty($service['ShippingTimeMax'])) { + $max = (int)$service['ShippingTimeMax']; + } else { + $max = 21; + } + + $this->db->query(" + INSERT INTO `" . DB_PREFIX . "ebay_shipping` SET + `description` = '" . $this->db->escape((string)$service['Description']) . "', + `InternationalService` = '" . (int)$service['InternationalService'] . "', + `ShippingService` = '" . $this->db->escape((string)$service['ShippingService']) . "' , + `ShippingServiceID` = '" . (int)$service['ShippingServiceID'] . "', + `ServiceType` = '" . $this->db->escape(strtolower(implode(',', $service['ServiceType']))) . "' , + `ValidForSellingFlow` = '" . (int)$service['ValidForSellingFlow'] . "', + `ShippingCategory` = '" . $this->db->escape((string)$service['ShippingCategory']) . "' , + `ShippingTimeMin` = '" . (int)$min . "', + `ShippingTimeMax` = '" . (int)$max . "' + "); + } + $this->log('Populated ebay_shipping table'); + } else { + $this->log('No shiopping details set!'); + } + + //shipping locations + if (isset($response['shipping_location'])) { + $this->db->query("TRUNCATE TABLE `" . DB_PREFIX . "ebay_shipping_location`"); + $this->log('Emptied ebay_shipping_location table'); + foreach ($response['shipping_location'] as $service) { + $this->db->query(" + INSERT INTO `" . DB_PREFIX . "ebay_shipping_location` + SET + `description` = '" . $this->db->escape((string)$service['Description']) . "', + `detail_version` = '" . $this->db->escape($service['DetailVersion']) . "', + `shipping_location` = '" . $this->db->escape((string)$service['ShippingLocation']) . "' , + `update_time` = '" . (int)$service['UpdateTime'] . "' + "); + } + $this->log('Populated ebay_shipping_location table'); + } else { + $this->log('No shipping locations set!'); + } + + //shipping locations exclude + if (isset($response['exclude_shipping_location'])) { + $this->db->query("TRUNCATE TABLE `" . DB_PREFIX . "ebay_shipping_location_exclude`"); + $this->log('Emptied ebay_shipping_location_exclude table'); + foreach ($response['exclude_shipping_location'] as $service) { + $this->db->query(" + INSERT INTO `" . DB_PREFIX . "ebay_shipping_location_exclude` + SET + `description` = '" . $this->db->escape((string)$service['Description']) . "', + `location` = '" . $this->db->escape((string)$service['Location']) . "', + `region` = '" . $this->db->escape((string)$service['Region']) . "' + "); + } + $this->log('Populated exclude_shipping_location table'); + } else { + $this->log('No shipping exclude locations set!'); + } + + //max dispatch times + if (isset($response['dispatch_time_max'])) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = 'dispatch_time_max' LIMIT 1"); + + if ($qry->num_rows > 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_setting_option` SET `data` = '" . $this->db->escape(serialize($response['dispatch_time_max'])) . "', `last_updated` = now() WHERE `key` = 'dispatch_time_max' LIMIT 1"); + $this->log('Updated dispatch_time_max into ebay_setting_option table'); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_setting_option` SET `key` = 'dispatch_time_max', `data` = '" . $this->db->escape(serialize($response['dispatch_time_max'])) . "', `last_updated` = now()"); + $this->log('Inserted dispatch_time_max into ebay_setting_option table'); + } + } else { + $this->log('No dispatch_time_max set!'); + } + + //countries + if (isset($response['countries'])) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = 'countries' LIMIT 1"); + + if ($qry->num_rows > 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_setting_option` SET `data` = '" . $this->db->escape(serialize($response['countries'])) . "', `last_updated` = now() WHERE `key` = 'countries' LIMIT 1"); + $this->log('Updated countries into ebay_setting_option table'); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_setting_option` SET `key` = 'countries', `data` = '" . $this->db->escape(serialize($response['countries'])) . "', `last_updated` = now()"); + $this->log('Inserted countries into ebay_setting_option table'); + } + } else { + $this->log('No countries set!'); + } + + //returns + if (isset($response['returns'])) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = 'returns' LIMIT 1"); + + if ($qry->num_rows > 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_setting_option` SET `data` = '" . $this->db->escape(serialize($response['returns'])) . "', `last_updated` = now() WHERE `key` = 'returns' LIMIT 1"); + $this->log('Updated returns info in to ebay_setting_option table'); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_setting_option` SET `key` = 'returns', `data` = '" . $this->db->escape(serialize($response['returns'])) . "', `last_updated` = now()"); + $this->log('Inserted returns info in to ebay_setting_option table'); + } + } else { + $this->log('No returns set!'); + } + + //package sizes + if (isset($response['package_type'])) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = 'package_type' LIMIT 1"); + + if ($qry->num_rows > 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_setting_option` SET `data` = '" . $this->db->escape(serialize($response['package_type'])) . "', `last_updated` = now() WHERE `key` = 'package_type' LIMIT 1"); + $this->log('Updated package_type info in to ebay_setting_option table'); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_setting_option` SET `key` = 'package_type', `data` = '" . $this->db->escape(serialize($response['package_type'])) . "', `last_updated` = now()"); + $this->log('Inserted package_type info in to ebay_setting_option table'); + } + } else { + $this->log('No package_type set!'); + } + + //vat enabled + if (isset($response['vat_enabled'])) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = 'vat_enabled' LIMIT 1"); + + if ($qry->num_rows > 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_setting_option` SET `data` = '" . (int)$response['package_type'] . "', `last_updated` = now() WHERE `key` = 'vat_enabled' LIMIT 1"); + $this->log('Updated vat_enabled in to ebay_setting_option table'); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_setting_option` SET `key` = 'vat_enabled', `data` = '" . (int)$response['package_type'] . "', `last_updated` = now()"); + $this->log('Inserted vat_enabled info in to ebay_setting_option table'); + } + } else { + $this->log('No vat_enabled set!'); + } + + //shipping types + if (isset($response['shipping_types'])) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = 'shipping_types' LIMIT 1"); + + if ($qry->num_rows > 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_setting_option` SET `data` = '" . $this->db->escape(serialize($response['shipping_types'])) . "', `last_updated` = now() WHERE `key` = 'shipping_types' LIMIT 1"); + $this->log('Updated shipping_types info in to ebay_setting_option table'); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_setting_option` SET `key` = 'shipping_types', `data` = '" . $this->db->escape(serialize($response['shipping_types'])) . "', `last_updated` = now()"); + $this->log('Inserted shipping_types info in to ebay_setting_option table'); + } + } else { + $this->log('No shipping_types set!'); + } + + //measurement types + if (isset($response['measurement_types'])) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = 'measurement_types' LIMIT 1"); + + if ($qry->num_rows > 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_setting_option` SET `data` = '" . $this->db->escape(serialize($response['measurement_types'])) . "', `last_updated` = now() WHERE `key` = 'measurement_types' LIMIT 1"); + $this->log('Updated measurement_types info in to ebay_setting_option table'); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_setting_option` SET `key` = 'measurement_types', `data` = '" . $this->db->escape(serialize($response['measurement_types'])) . "', `last_updated` = now()"); + $this->log('Inserted measurement_types info in to ebay_setting_option table'); + } + } else { + $this->log('No measurement_types set!'); + } + + // Product details + if (isset($response['product_details'])) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_setting_option` WHERE `key` = 'product_details' LIMIT 1"); + + if ($qry->num_rows > 0) { + $this->db->query("UPDATE `" . DB_PREFIX . "ebay_setting_option` SET `data` = '" . $this->db->escape(serialize($response['product_details'])) . "', `last_updated` = now() WHERE `key` = 'product_details' LIMIT 1"); + $this->log('Updated product_details info in to ebay_setting_option table'); + } else { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_setting_option` SET `key` = 'product_details', `data` = '" . $this->db->escape(serialize($response['product_details'])) . "', `last_updated` = now()"); + $this->log('Inserted product_details info in to ebay_setting_option table'); + } + } else { + $this->log('No product_details set!'); + } + } + + return array('msg' => $this->lastmsg, 'error' => $this->lasterror); + } + + public function updateStore() { + $store = $this->call('setup/getSellerStore/', array(), array(), 'json', true); + + if ($this->lasterror != true) { + if ($store['store'] == true) { + $this->db->query("DROP TABLE IF EXISTS `" . DB_PREFIX . "ebay_store_category`;"); + $this->db->query(" + CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "ebay_store_category` ( + `ebay_store_category_id` int(11) NOT NULL AUTO_INCREMENT, + `parent_id` int(11) NOT NULL, + `CategoryID` char(100) NOT NULL, + `CategoryName` char(100) NOT NULL, + PRIMARY KEY (`ebay_store_category_id`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;"); + + if (!empty($store['settings']['categories'])) { + foreach ($store['settings']['categories'] as $cat1) { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_store_category` SET `CategoryID` = '" . $this->db->escape($cat1['id']) . "', `CategoryName` = '" . $this->db->escape($cat1['name']) . "'"); + $id1 = $this->db->getLastId(); + + if (!empty($cat1['children'])) { + foreach ($cat1['children'] as $cat2) { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_store_category` SET `CategoryID` = '" . $this->db->escape($cat2['id']) . "', `CategoryName` = '" . $this->db->escape($cat2['name']) . "', `parent_id` = '" . $this->db->escape($id1) . "'"); + $id2 = $this->db->getLastId(); + + if (!empty($cat2['children'])) { + foreach ($cat2['children'] as $cat3) { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_store_category` SET `CategoryID` = '" . $this->db->escape($cat3['id']) . "', `CategoryName` = '" . $this->db->escape($cat3['name']) . "', `parent_id` = '" . $this->db->escape($id2) . "'"); + $id3 = $this->db->getLastId(); + + if (!empty($cat3['children'])) { + foreach ($cat3['children'] as $cat4) { + $this->db->query("INSERT INTO `" . DB_PREFIX . "ebay_store_category` SET `CategoryID` = '" . $this->db->escape($cat4['id']) . "', `CategoryName` = '" . $this->db->escape($cat4['name']) . "', `parent_id` = '" . $this->db->escape($id3) . "'"); + $id4 = $this->db->getLastId(); + } + } + } + } + } + } + } + } + } + } + + return array('msg' => $this->lastmsg, 'error' => $this->lasterror); + } + + public function editSetting($group, $data, $store_id = 0) { + $this->db->query("DELETE FROM " . DB_PREFIX . "setting WHERE store_id = '" . (int)$store_id . "' AND `code` = '" . $this->db->escape($group) . "'"); + + foreach ($data as $key => $value) { + if (!is_array($value)) { + $this->db->query("INSERT INTO " . DB_PREFIX . "setting SET store_id = '" . (int)$store_id . "', `code` = '" . $this->db->escape($group) . "', `key` = '" . $this->db->escape($key) . "', `value` = '" . $this->db->escape($value) . "'"); + } else { + $this->db->query("INSERT INTO " . DB_PREFIX . "setting SET store_id = '" . (int)$store_id . "', `code` = '" . $this->db->escape($group) . "', `key` = '" . $this->db->escape($key) . "', `value` = '" . $this->db->escape(serialize($value)) . "', serialized = '1'"); + } + } + } + + public function getShippingServiceInfo($service_code) { + $qry = $this->db->query("SELECT * FROM `" . DB_PREFIX . "ebay_shipping` WHERE `ShippingService` = '" . $this->db->escape($service_code) . "' LIMIT 1"); + + if ($qry->num_rows) { + return $qry->row; + } else { + return false; + } + } +} diff --git a/public/system/library/openbay/etsy.php b/public/system/library/openbay/etsy.php new file mode 100644 index 0000000..bf18120 --- /dev/null +++ b/public/system/library/openbay/etsy.php @@ -0,0 +1,459 @@ +<?php +namespace openbay; + +final class Etsy { + private $token; + private $encryption_key; + private $encryption_iv; + private $url = 'https://api.openbaypro.io/'; + private $registry; + private $logger; + private $max_log_size = 50; //max log size in Mb + + public function __construct($registry) { + $this->registry = $registry; + $this->token = $this->config->get('etsy_token'); + $this->logging = $this->config->get('etsy_logging'); + + if ($this->logging == 1) { + $this->setLogger(); + } + + $this->setEncryptionKey($this->config->get('etsy_encryption_key')); + $this->setEncryptionIv($this->config->get('etsy_encryption_iv')); + } + + public function __get($name) { + return $this->registry->get($name); + } + + public function getEncryptionKey() { + return $this->encryption_key; + } + + public function setEncryptionKey($key) { + $this->encryption_key = $key; + } + + public function getEncryptionIv() { + return $this->encryption_iv; + } + + public function setEncryptionIv($encryption_iv) { + $this->encryption_iv = $encryption_iv; + } + + public function resetConfig($token, $encryption_key) { + $this->token = $token; + $this->setEncryptionKey($encryption_key); + } + + public function call($uri, $method, $data = array()) { + if($this->config->get('etsy_status') == 1) { + $headers = array (); + $headers[] = 'X-Auth-Token: ' . $this->token; + $headers[] = 'X-Endpoint-Version: 2'; + $headers[] = 'Content-Type: application/json'; + //$headers[] = 'Content-Length: '.strlen(json_encode($data)); + + $defaults = array( + CURLOPT_HEADER => 0, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_URL => $this->url . $uri, + CURLOPT_USERAGENT => "OpenBay Pro for Etsy/OpenCart", + CURLOPT_FRESH_CONNECT => 1, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FORBID_REUSE => 1, + CURLOPT_TIMEOUT => 180, + CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_SSL_VERIFYHOST => 0, + //CURLOPT_VERBOSE => true, + //CURLOPT_STDERR => fopen(DIR_LOGS . 'curl_verbose.log', "w+") + ); + + if ($method == 'POST') { + $defaults[CURLOPT_POST] = 1; + $defaults[CURLOPT_POSTFIELDS] = json_encode($data); + } + + $curl = curl_init(); + curl_setopt_array($curl, $defaults); + + $response = array(); + + if (! $result = curl_exec($curl)) { + $this->log('call() - Curl Failed ' . curl_error($curl) . ' ' . curl_errno($curl)); + + return false; + } else { + $this->log('call() - Result of : "' . print_r($result, true) . '"'); + + $encoding = mb_detect_encoding($result); + + if($encoding == 'UTF-8') { + $result = preg_replace('/[^(\x20-\x7F)]*/', '', $result); + } + + $result = json_decode($result, 1); + + $response['header_code'] = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if(!empty($result)) { + $response['data'] = $result; + } else { + $response['data'] = ''; + } + } + + curl_close($curl); + + return $response; + } else { + $this->log('call() - OpenBay Pro / Etsy not active'); + + return false; + } + } + + private function setLogger() { + if (file_exists(DIR_LOGS . 'etsylog.log')) { + if (filesize(DIR_LOGS . 'etsylog.log') > ($this->max_log_size * 1000000)) { + rename(DIR_LOGS . 'etsylog.log', DIR_LOGS . '_etsylog_' . date('Y-m-d_H-i-s') . '.log'); + } + } + + $this->logger = new \Log('etsylog.log'); + } + + public function log($data, $write = true) { + if ($this->logging == 1) { + if (function_exists('getmypid')) { + $process_id = getmypid(); + $data = $process_id . ' - ' . print_r($data, true); + } + + $this->logger->write($data); + } + } + + public function getServer() { + return $this->url; + } + + public function settingsUpdate() { + $this->log("settingsUpdate() - start"); + + $response = $this->call('v1/etsy/data/type/getSetup/', 'GET'); + + if (isset($response['data']) && is_array($response['data'])) { + foreach ($response['data'] as $key => $options) { + $this->db->query("DELETE FROM `" . DB_PREFIX . "etsy_setting_option` WHERE `key` = '" . $this->db->escape($key) . "' LIMIT 1"); + + $this->db->query("INSERT INTO `" . DB_PREFIX . "etsy_setting_option` SET `data` = '" . $this->db->escape(json_encode($options)) . "', `key` = '" . $this->db->escape($key) . "', `last_updated` = now()"); + + $this->log("settingsUpdate() - updated option: " . $key); + } + + $this->log("settingsUpdate() - complete"); + } else { + $this->log("settingsUpdate() - failed - no data response"); + } + } + + public function getSetting($key) { + $this->log("getSetting() - " . $key); + + $qry = $this->db->query("SELECT `data` FROM `" . DB_PREFIX . "etsy_setting_option` WHERE `key` = '" . $this->db->escape($key) . "' LIMIT 1"); + + if($qry->num_rows > 0) { + $this->log("getSetting() - Found setting"); + + return json_decode($qry->row['data']); + } else { + return false; + } + } + + public function getLinks($product_id, $status = 0, $limit = null) { + $this->log("getLinks() - Product_id: " . $product_id . " status: " . $status . " limit:" . $limit); + + if ($limit != null) { + $sql_limit = ' LIMIT 1'; + } else { + $sql_limit = ''; + } + + $qry = $this->db->query("SELECT `el`.*, `p`.`quantity` FROM `" . DB_PREFIX . "etsy_listing` `el` LEFT JOIN `" . DB_PREFIX . "product` `p` ON `el`.`product_id` = `p`.`product_id` WHERE `el`.`product_id` = '" . (int)$product_id . "' AND `el`.`status` = '" . (int)$status . "' ORDER BY `el`.`created` DESC" . $sql_limit); + + if ($qry->num_rows) { + $this->log("getLinks() - " . $qry->num_rows . " found"); + + $links = array(); + + foreach ($qry->rows as $row) { + $links[] = $row; + } + + return $links; + } else { + $this->log("getLinks() - no links found"); + + return false; + } + } + + public function getLinkedProduct($etsy_item_id) { + $this->log("getLinkedProduct() - etsy_item_id: " . $etsy_item_id); + + $qry = $this->db->query("SELECT `p`.`quantity`, `p`.`product_id`, `p`.`model`, `el`.`etsy_listing_id`, `el`.`status` AS `link_status` FROM `" . DB_PREFIX . "etsy_listing` `el` LEFT JOIN `" . DB_PREFIX . "product` `p` ON `p`.`product_id` = `el`.`product_id` WHERE `el`.`etsy_item_id` = '" . (int)$etsy_item_id . "' AND `el`.`status` = 1"); + + if($qry->num_rows) { + $this->log("getLinkedProduct() - " . $qry->num_rows . " found"); + + return $qry->row; + } else { + $this->log("getLinkedProduct() - no link found"); + + return false; + } + } + + public function updateListingStock($etsy_item_id, $new_stock, $status) { + $this->log("updateListingStock() - ItemID: " . $etsy_item_id . ", new stock: " . $new_stock . ", status: " . $status); + + if ($new_stock > 0) { + $this->log("updateListingStock() - stock > 0 - update stock"); + + if ($status == 'edit') { + $status = 'inactive'; + } + + $response = $this->call('v1/etsy/product/listing/' . (int)$etsy_item_id . '/updateStock/', 'POST', array('quantity' => $new_stock, 'state' => $status)); + + if (isset($response['data']['error'])) { + return $response; + } else { + return true; + } + } else { + $this->log("updateListingStock() - stock > 0 - set to inactive"); + + $this->deleteLink(null, $etsy_item_id); + + $response = $this->call('v1/etsy/product/listing/' . (int)$etsy_item_id . '/inactive/', 'POST'); + + if (isset($response['data']['error'])) { + $this->log("updateListingStock() - Error: " . json_encode($response)); + + return $response; + } else { + $this->log("updateListingStock() - Item ended OK"); + + return true; + } + } + } + + public function deleteProduct($product_id) { + $this->log("deleteProduct() - Product ID: " . $product_id); + + $this->db->query("DELETE FROM `" . DB_PREFIX . "etsy_listing` WHERE `product_id` = '" . (int)$product_id . "'"); + } + + public function deleteLink($etsy_listing_id = null, $etsy_item_id = null) { + $this->log("deleteLink() - Listing ID: " . $etsy_listing_id . ", item ID" . $etsy_item_id); + + if ($etsy_listing_id != null) { + $this->log("deleteLink() - Listing ID is not null"); + + $this->db->query("UPDATE `" . DB_PREFIX . "etsy_listing` SET `status` = 0 WHERE `etsy_listing_id` = '" . (int)$etsy_listing_id . "' LIMIT 1"); + } elseif ($etsy_item_id != null) { + $this->log("deleteLink() - Item ID is not null"); + + $this->db->query("UPDATE `" . DB_PREFIX . "etsy_listing` SET `status` = 0 WHERE `etsy_item_id` = '" . (int)$etsy_item_id . "' LIMIT 1"); + } + } + + public function productUpdateListen($product_id, $data = array()) { + $this->log("productUpdateListen() - " . $product_id . ", Data: " . json_encode($data)); + + $links = $this->getLinks($product_id, 1); + + if (!empty($links)) { + foreach ($links as $link) { + $this->log("productUpdateListen() - Item ID: " . $link['etsy_item_id']); + + $etsy_listing = $this->getEtsyItem($link['etsy_item_id']); + + if ($etsy_listing != false && isset($etsy_listing['state']) && ($etsy_listing['state'] == 'active' || $etsy_listing['state'] == 'private' || $etsy_listing['state'] == 'draft' || $etsy_listing['state'] == 'edit')) { + $this->log("productUpdateListen() - Listing state seems valid"); + + if ($etsy_listing['quantity'] != $link['quantity']) { + $this->log("productUpdateListen() - Stock is different, do update"); + + $this->updateListingStock($link['etsy_item_id'], $link['quantity'], $etsy_listing['state']); + } else { + $this->log("productUpdateListen() - Stock is the same: " . $etsy_listing['quantity'] . " " . $link['quantity']); + } + } else { + $this->log("productUpdateListen() - Listing state seems invalid"); + $this->log("productUpdateListen() - " . json_encode($etsy_listing)); + + $this->deleteLink($link['etsy_listing_id']); + } + } + } else { + $this->log("productUpdateListen() - No links"); + } + } + + public function orderFind($order_id = null, $receipt_id = null) { + $this->log("orderFind() - OrderID: " . $order_id . ", Receipt ID: " . $receipt_id); + + if ($order_id != null) { + $this->log("orderFind() - Order ID is not null"); + + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "etsy_order` WHERE `order_id` = '" . (int)$order_id . "' LIMIT 1"); + + if($query->num_rows > 0) { + $this->log('orderFind() - Found'); + return $query->row; + } else { + $this->log('orderFind() - Not found'); + return false; + } + } elseif ($receipt_id != null) { + $this->log("orderFind() - Receipt ID is not null"); + + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "etsy_order` WHERE `receipt_id` = '" . (int)$receipt_id . "' LIMIT 1"); + + if($query->num_rows > 0) { + $this->log('orderFind() - Found'); + return $query->row; + } else { + $this->log('orderFind() - Not found'); + return false; + } + } + } + + public function orderDelete($order_id) { + $this->log("orderDelete() - ID: " . $order_id); + + if(!$this->orderFind($order_id)) { + $query = $this->db->query("SELECT `p`.`product_id` FROM `" . DB_PREFIX . "order_product` `op` LEFT JOIN `" . DB_PREFIX . "product` `p` ON `op`.`product_id` = `p`.`product_id` WHERE `op`.`order_id` = '" . (int)$order_id . "'"); + + if($query->num_rows > 0) { + $this->log("orderDelete() - " . $query->num_rows . " products"); + + foreach ($query->rows as $product) { + $this->log("orderDelete() - Processing ID: " . $product['product_id']); + + $this->productUpdateListen((int)$product['product_id']); + } + } else { + $this->log("orderDelete() - No products in order"); + } + } else { + $this->log("orderDelete() - Not an Etsy order"); + } + } + + public function orderUpdatePaid($receipt_id, $status) { + $this->log("orderUpdatePaid() - Receipt ID: " . $receipt_id . ", Status: " . $status); + + $response = $this->openbay->etsy->call('v1/etsy/order/update/payment/', 'POST', array('receipt_id' => $receipt_id, 'status' => $status)); + + if (isset($response['data']['error'])) { + $this->log("orderUpdatePaid() - Error: " . json_encode($response)); + + return $response; + } else { + $this->log("orderUpdatePaid() - OK"); + + return true; + } + } + + public function orderUpdateShipped($receipt_id, $status) { + $this->log("orderUpdateShipped() - Receipt ID: " . $receipt_id . ", Status: " . $status); + + $response = $this->openbay->etsy->call('v1/etsy/order/update/shipping/', 'POST', array('receipt_id' => $receipt_id, 'status' => $status)); + + if (isset($response['data']['error'])) { + $this->log("orderUpdateShipped() - Error: " . json_encode($response)); + + return $response; + } else { + $this->log("orderUpdateShipped() - OK"); + + return true; + } + } + + public function putStockUpdateBulk($product_id_array, $end_inactive) { + $this->log("putStockUpdateBulk() - ok"); + $this->log("putStockUpdateBulk() - Item count: " . count($product_id_array)); + + foreach($product_id_array as $product_id) { + $this->log("putStockUpdateBulk() - Product ID: " . $product_id); + + $links = $this->getLinks($product_id, 1); + + if (!empty($links)) { + $this->log("putStockUpdateBulk() - Links found: " . count($links)); + + foreach ($links as $link) { + $etsy_listing = $this->getEtsyItem($link['etsy_item_id']); + + if ($etsy_listing != false && isset($etsy_listing['state']) && ($etsy_listing['state'] == 'active' || $etsy_listing['state'] == 'private' || $etsy_listing['state'] == 'draft' || $etsy_listing['state'] == 'edit')) { + $this->log("putStockUpdateBulk() - Listing state seems valid"); + + if ($etsy_listing['quantity'] != $link['quantity']) { + $this->log("putStockUpdateBulk() - Stock is different, do update"); + + $this->updateListingStock($link['etsy_item_id'], $link['quantity'], $etsy_listing['state']); + } else { + $this->log("putStockUpdateBulk() - Stock is the same: " . $etsy_listing['quantity'] . " " . $link['quantity']); + } + } else { + $this->log("putStockUpdateBulk() - Listing state seems invalid"); + $this->log("putStockUpdateBulk() - " . json_encode($etsy_listing)); + + $this->deleteLink($link['etsy_listing_id']); + } + } + } else { + $this->log("putStockUpdateBulk() - No link found"); + } + } + } + + public function getEtsyItem($listing_id) { + $this->log("getEtsyItem(): " . $listing_id); + + $response = $this->openbay->etsy->call('v1/etsy/product/listing/' . $listing_id . '/', 'GET'); + + if (isset($response['data']['error'])) { + $this->log("getEtsyItem() error: ". $response['data']['error']); + + return $response; + } else { + $this->log("getEtsyItem() - OK : " . json_encode($response)); + + return $response['data']['results'][0]; + } + } + + public function validate() { + if ($this->config->get('etsy_token') && $this->config->get('etsy_encryption_key') && $this->config->get('etsy_encryption_iv')) { + $this->log("Etsy details valid"); + + return true; + } else { + $this->log("Etsy details are not valid"); + + return false; + } + } +} diff --git a/public/system/library/openbay/fba.php b/public/system/library/openbay/fba.php new file mode 100644 index 0000000..aded084 --- /dev/null +++ b/public/system/library/openbay/fba.php @@ -0,0 +1,305 @@ +<?php +namespace openbay; + +final class fba { + private $api_key; + private $api_account_id; + private $encryption_key; + private $encryption_iv; + private $url = 'https://api.openbaypro.io/'; + private $registry; + + private $logging = 1; + private $logging_verbose = 1; + private $max_log_size = 50; + + /** + * Status IDs = + * 0 = new + * 1 = error + * 2 = held + * 3 = shipped + * 4 = cancelled + */ + + /** + * Type IDs = + * 0 = new + * 1 = shipping + * 2 = cancel + */ + + public function __construct($registry) { + $this->registry = $registry; + + $this->api_key = $this->config->get('openbay_fba_api_key'); + $this->api_account_id = $this->config->get('openbay_fba_api_account_id'); + $this->logging = $this->config->get('openbay_fba_debug_log'); + + $this->setEncryptionKey($this->config->get('openbay_fba_encryption_key')); + $this->setEncryptionIv($this->config->get('openbay_fba_encryption_iv')); + + if ($this->logging == 1) { + $this->setLogger(); + } + } + + public function __get($name) { + return $this->registry->get($name); + } + + public function getEncryptionKey() { + return $this->encryption_key; + } + + public function setEncryptionKey($key) { + $this->encryption_key = $key; + } + + public function getEncryptionIv() { + return $this->encryption_iv; + } + + public function setEncryptionIv($encryption_iv) { + $this->encryption_iv = $encryption_iv; + } + + public function setApiKey($api_key) { + $this->api_key = $api_key; + } + + public function setAccountId($api_account_id) { + $this->api_account_id = $api_account_id; + } + + public function call($uri, $data = array(), $request_type = 'GET') { + $this->log("Request: " . $request_type . " : " . $this->url . $uri); + + $headers = array(); + $headers[] = 'X-Auth-Token: ' . $this->api_key; + $headers[] = 'X-Account-ID: ' . $this->api_account_id; + $headers[] = 'X-Endpoint-Version: 2'; + $headers[] = 'Content-Type: application/json'; + + $defaults = array( + CURLOPT_HEADER => 0, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_URL => $this->url . $uri, + CURLOPT_USERAGENT => "OpenBay Pro for Fulfillment by Amazon/OpenCart", + CURLOPT_FRESH_CONNECT => 1, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_FORBID_REUSE => 1, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_SSL_VERIFYHOST => 0, + ); + + if ($this->logging_verbose == 1) { + $defaults[CURLOPT_VERBOSE] = 1; + $defaults[CURLOPT_STDERR] = fopen(DIR_LOGS . 'fba_verbose.log', "a+"); + } + + if ($request_type == "POST") { + $this->log('Request body:'); + $this->log(print_r($data, true)); + $defaults[CURLOPT_POST] = json_encode($data); + $defaults[CURLOPT_POSTFIELDS] = json_encode($data); + } else { + $defaults[CURLOPT_CUSTOMREQUEST] = "GET"; + } + + $curl = curl_init(); + + curl_setopt_array($curl, $defaults); + + $result = curl_exec($curl); + + if (!$result) { + $this->log('call() - Curl Failed ' . curl_error($curl) . ' ' . curl_errno($curl)); + + $response = array('error' => true, 'error_messages' => array(curl_error($curl) . ' ' . curl_errno($curl)), 'body' => null, 'response_http' => 0); + } else { + $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + $this->log("Response: " . $http_code . " : " . strlen($result) . " bytes"); + + $encoding = mb_detect_encoding($result); + + if ($encoding == 'UTF-8') { + $result = preg_replace('/[^(\x20-\x7F)]*/', '', $result); + } + + $result_parsed = json_decode($result, 1); + + $this->log('Raw json response:'); + $this->log($result); + + $this->log('Parsed response:'); + $this->log(print_r($result_parsed, true)); + + $response = array( + 'error' => false, + 'error_messages' => array(), + 'body' => (isset($result_parsed['result']) ? $result_parsed['result'] : ''), + 'response_http' => $http_code + ); + + if (isset($result_parsed['errors']) && !empty($result_parsed['errors'])) { + $response['error'] = true; + $response['error_messages'] = $result_parsed['errors']; + } + } + + curl_close($curl); + + return $response; + } + + public function getServerUrl() { + return $this->url; + } + + public function validate() { + if ($this->config->get('openbay_fba_api_account_id') && $this->config->get('openbay_fba_api_key') && $this->config->get('openbay_fba_encryption_key') && $this->config->get('openbay_fba_encryption_iv')) { + return true; + } else { + return false; + } + } + + private function setLogger() { + if(file_exists(DIR_LOGS . 'fulfillment_by_amazon.log')) { + if(filesize(DIR_LOGS . 'fulfillment_by_amazon.log') > ($this->max_log_size * 1000000)) { + rename(DIR_LOGS . 'fulfillment_by_amazon.log', DIR_LOGS . '_fulfillment_by_amazon_' . date('Y-m-d_H-i-s') . '.log'); + } + } + + $this->logger = new \Log('fulfillment_by_amazon.log'); + } + + public function log($data) { + if ($this->logging == 1) { + if (function_exists('getmypid')) { + $process_id = getmypid(); + $data = $process_id . ' - ' . $data; + } + + $this->logger->write($data); + } + } + + public function createFBAOrderID($order_id) { + $this->db->query("INSERT INTO `" . DB_PREFIX . "fba_order` SET `order_id` = '" . (int)$order_id . "', `status` = 0, `created` = now()"); + + return $this->db->getLastId(); + } + + public function updateFBAOrderStatus($order_id, $status_id) { + $this->db->query("UPDATE `" . DB_PREFIX . "fba_order` SET `status` = '" . (int)$status_id . "' WHERE `order_id` = '" . (int)$order_id . "' LIMIT 1"); + } + + public function updateFBAOrderRef($order_id, $ref) { + $this->db->query("UPDATE `" . DB_PREFIX . "fba_order` SET `fba_order_fulfillment_ref` = '" . $this->db->escape($ref) . "' WHERE `order_id` = '" . (int)$order_id . "' LIMIT 1"); + } + + public function updateFBAOrderFulfillmentID($order_id, $fba_order_fulfillment_id) { + $this->db->query("UPDATE `" . DB_PREFIX . "fba_order` SET `fba_order_fulfillment_id` = '" . (int)$fba_order_fulfillment_id . "' WHERE `order_id` = '" . (int)$order_id . "'"); + } + + public function createFBAFulfillmentID($order_id, $type) { + $this->db->query("INSERT INTO `" . DB_PREFIX . "fba_order_fulfillment` SET `created` = now(), `order_id` = '" . (int)$order_id . "', `type` = '" . (int)$type . "'"); + + $id = $this->db->getLastId(); + + $this->db->query("UPDATE `" . DB_PREFIX . "fba_order` SET `fba_order_fulfillment_id` = '" . (int)$id . "' WHERE `order_id` = '" . (int)$order_id . "' LIMIT 1"); + + return $id; + } + + public function populateFBAFulfillment($request_body, $response_body, $header_code, $fba_order_fulfillment_id) { + $this->db->query(" + UPDATE `" . DB_PREFIX . "fba_order_fulfillment` + SET + `request_body` = '" . $this->db->escape($request_body) . "', + `response_body` = '" . $this->db->escape($response_body) . "', + `response_header_code` = '" . (int)$header_code . "' + WHERE + `fba_order_fulfillment_id` = '" . (int)$fba_order_fulfillment_id . "' + "); + + $insert_id = $this->db->getLastId(); + + return $insert_id; + } + + public function getFBAOrders($filter) { + $sql = ""; + + // start date filter + if (isset($filter['filter_start'])) { + $sql .= " AND `created` >= '".$filter['filter_start']."'"; + } + // end date filter + if (isset($filter['filter_end'])) { + $sql .= " AND `created` <= '".$filter['filter_end']."'"; + } + // status filter + if (isset($filter['filter_status'])) { + $sql .= " AND `status` = '".$filter['filter_status']."'"; + } + + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "fba_order` WHERE 1 ".$sql." ORDER BY `created` DESC"); + + if ($query->num_rows == 0) { + return false; + } else { + return $query->rows; + } + } + + public function getFBAOrder($order_id) { + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "fba_order` WHERE `order_id` = '" . (int)$order_id . "' LIMIT 1"); + + if ($query->num_rows == 0) { + return false; + } else { + $fba_order = $query->row; + $fba_order['fulfillments'] = $this->getFBAOrderFulfillments($order_id); + + return $fba_order; + } + } + + public function getFBAOrderByRef($ref) { + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "fba_order` WHERE `fba_order_fulfillment_ref` = '" . $this->db->escape($ref) . "' LIMIT 1"); + + if ($query->num_rows == 0) { + return false; + } else { + $fba_order = $query->row; + $fba_order['fulfillments'] = $fba_order['order_id']; + + return $fba_order; + } + } + + public function getFBAOrderFulfillments($order_id) { + $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "fba_order_fulfillment` WHERE `order_id` = '" . (int)$order_id . "' ORDER BY `created` DESC"); + + if ($query->num_rows == 0) { + return false; + } else { + return $query->rows; + } + } + + public function hasOrderFBAItems($order_id) { + $query = $this->db->query("SELECT COUNT(*) AS `total` FROM `" . DB_PREFIX . "order_product` `op` LEFT JOIN `" . DB_PREFIX . "product` `p` ON `op`.`product_id` = `p`.`product_id` WHERE `p`.`location` = 'FBA' AND `op`.`order_id` = '".(int)$order_id."'"); + + if ($query->num_rows == 0) { + return false; + } else { + return $query->row['total']; + } + } +} diff --git a/public/system/library/pagination.php b/public/system/library/pagination.php new file mode 100644 index 0000000..39f8575 --- /dev/null +++ b/public/system/library/pagination.php @@ -0,0 +1,106 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Pagination class +*/ +class Pagination { + public $total = 0; + public $page = 1; + public $limit = 20; + public $num_links = 8; + public $url = ''; + public $text_first = '|<'; + public $text_last = '>|'; + public $text_next = '>'; + public $text_prev = '<'; + + /** + * + * + * @return text + */ + public function render() { + $total = $this->total; + + if ($this->page < 1) { + $page = 1; + } else { + $page = $this->page; + } + + if (!(int)$this->limit) { + $limit = 10; + } else { + $limit = $this->limit; + } + + $num_links = $this->num_links; + $num_pages = ceil($total / $limit); + + $this->url = str_replace('%7Bpage%7D', '{page}', $this->url); + + $output = '<ul class="pagination">'; + + if ($page > 1) { + $output .= '<li><a href="' . str_replace(array('&page={page}', '?page={page}', '&page={page}'), '', $this->url) . '">' . $this->text_first . '</a></li>'; + + if ($page - 1 === 1) { + $output .= '<li><a href="' . str_replace(array('&page={page}', '?page={page}', '&page={page}'), '', $this->url) . '">' . $this->text_prev . '</a></li>'; + } else { + $output .= '<li><a href="' . str_replace('{page}', $page - 1, $this->url) . '">' . $this->text_prev . '</a></li>'; + } + } + + if ($num_pages > 1) { + if ($num_pages <= $num_links) { + $start = 1; + $end = $num_pages; + } else { + $start = $page - floor($num_links / 2); + $end = $page + floor($num_links / 2); + + if ($start < 1) { + $end += abs($start) + 1; + $start = 1; + } + + if ($end > $num_pages) { + $start -= ($end - $num_pages); + $end = $num_pages; + } + } + + for ($i = $start; $i <= $end; $i++) { + if ($page == $i) { + $output .= '<li class="active"><span>' . $i . '</span></li>'; + } else { + if ($i === 1) { + $output .= '<li><a href="' . str_replace(array('&page={page}', '?page={page}', '&page={page}'), '', $this->url) . '">' . $i . '</a></li>'; + } else { + $output .= '<li><a href="' . str_replace('{page}', $i, $this->url) . '">' . $i . '</a></li>'; + } + } + } + } + + if ($page < $num_pages) { + $output .= '<li><a href="' . str_replace('{page}', $page + 1, $this->url) . '">' . $this->text_next . '</a></li>'; + $output .= '<li><a href="' . str_replace('{page}', $num_pages, $this->url) . '">' . $this->text_last . '</a></li>'; + } + + $output .= '</ul>'; + + if ($num_pages > 1) { + return $output; + } else { + return ''; + } + } +} diff --git a/public/system/library/request.php b/public/system/library/request.php new file mode 100644 index 0000000..9b63a31 --- /dev/null +++ b/public/system/library/request.php @@ -0,0 +1,51 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Request class +*/ +class Request { + public $get = array(); + public $post = array(); + public $cookie = array(); + public $files = array(); + public $server = array(); + + /** + * Constructor + */ + public function __construct() { + $this->get = $this->clean($_GET); + $this->post = $this->clean($_POST); + $this->request = $this->clean($_REQUEST); + $this->cookie = $this->clean($_COOKIE); + $this->files = $this->clean($_FILES); + $this->server = $this->clean($_SERVER); + } + + /** + * + * @param array $data + * + * @return array + */ + public function clean($data) { + if (is_array($data)) { + foreach ($data as $key => $value) { + unset($data[$key]); + + $data[$this->clean($key)] = $this->clean($value); + } + } else { + $data = htmlspecialchars($data, ENT_COMPAT, 'UTF-8'); + } + + return $data; + } +}
\ No newline at end of file diff --git a/public/system/library/response.php b/public/system/library/response.php new file mode 100644 index 0000000..e36d3c3 --- /dev/null +++ b/public/system/library/response.php @@ -0,0 +1,121 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Response class +*/ +class Response { + private $headers = array(); + private $level = 0; + private $output; + + /** + * Constructor + * + * @param string $header + * + */ + public function addHeader($header) { + $this->headers[] = $header; + } + + /** + * + * + * @param string $url + * @param int $status + * + */ + public function redirect($url, $status = 302) { + header('Location: ' . str_replace(array('&', "\n", "\r"), array('&', '', ''), $url), true, $status); + exit(); + } + + /** + * + * + * @param int $level + */ + public function setCompression($level) { + $this->level = $level; + } + + /** + * + * + * @return array + */ + public function getOutput() { + return $this->output; + } + + /** + * + * + * @param string $output + */ + public function setOutput($output) { + $this->output = $output; + } + + /** + * + * + * @param string $data + * @param int $level + * + * @return string + */ + private function compress($data, $level = 0) { + if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false)) { + $encoding = 'gzip'; + } + + if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== false)) { + $encoding = 'x-gzip'; + } + + if (!isset($encoding) || ($level < -1 || $level > 9)) { + return $data; + } + + if (!extension_loaded('zlib') || ini_get('zlib.output_compression')) { + return $data; + } + + if (headers_sent()) { + return $data; + } + + if (connection_status()) { + return $data; + } + + $this->addHeader('Content-Encoding: ' . $encoding); + + return gzencode($data, (int)$level); + } + + /** + * + */ + public function output() { + if ($this->output) { + $output = $this->level ? $this->compress($this->output, $this->level) : $this->output; + + if (!headers_sent()) { + foreach ($this->headers as $header) { + header($header, true); + } + } + + echo $output; + } + } +} diff --git a/public/system/library/session.php b/public/system/library/session.php new file mode 100644 index 0000000..2a8957d --- /dev/null +++ b/public/system/library/session.php @@ -0,0 +1,90 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Session class +*/ +class Session { + protected $adaptor; + protected $session_id; + public $data = array(); + + /** + * Constructor + * + * @param string $adaptor + * @param object $registry + */ + public function __construct($adaptor, $registry = '') { + $class = 'Session\\' . $adaptor; + + if (class_exists($class)) { + if ($registry) { + $this->adaptor = new $class($registry); + } else { + $this->adaptor = new $class(); + } + + register_shutdown_function(array($this, 'close')); + } else { + trigger_error('Error: Could not load cache adaptor ' . $adaptor . ' session!'); + exit(); + } + } + + /** + * + * + * @return string + */ + public function getId() { + return $this->session_id; + } + + /** + * + * + * @param string $session_id + * + * @return string + */ + public function start($session_id = '') { + if (!$session_id) { + if (function_exists('random_bytes')) { + $session_id = substr(bin2hex(random_bytes(26)), 0, 26); + } else { + $session_id = substr(bin2hex(openssl_random_pseudo_bytes(26)), 0, 26); + } + } + + if (preg_match('/^[a-zA-Z0-9,\-]{22,52}$/', $session_id)) { + $this->session_id = $session_id; + } else { + exit('Error: Invalid session ID!'); + } + + $this->data = $this->adaptor->read($session_id); + + return $session_id; + } + + /** + * + */ + public function close() { + $this->adaptor->write($this->session_id, $this->data); + } + + /** + * + */ + public function __destroy() { + $this->adaptor->destroy($this->session_id); + } +} diff --git a/public/system/library/session/db.php b/public/system/library/session/db.php new file mode 100644 index 0000000..99882c7 --- /dev/null +++ b/public/system/library/session/db.php @@ -0,0 +1,49 @@ +<?php +/* +CREATE TABLE IF NOT EXISTS `session` ( + `session_id` varchar(32) NOT NULL, + `data` text NOT NULL, + `expire` datetime NOT NULL, + PRIMARY KEY (`session_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; +*/ +namespace Session; +final class DB { + public $expire = ''; + + public function __construct($registry) { + $this->db = $registry->get('db'); + + $this->expire = ini_get('session.gc_maxlifetime'); + } + + public function read($session_id) { + $query = $this->db->query("SELECT `data` FROM `" . DB_PREFIX . "session` WHERE session_id = '" . $this->db->escape($session_id) . "' AND expire > " . (int)time()); + + if ($query->num_rows) { + return json_decode($query->row['data'], true); + } else { + return false; + } + } + + public function write($session_id, $data) { + if ($session_id) { + $this->db->query("REPLACE INTO `" . DB_PREFIX . "session` SET session_id = '" . $this->db->escape($session_id) . "', `data` = '" . $this->db->escape(json_encode($data)) . "', expire = '" . $this->db->escape(date('Y-m-d H:i:s', time() + $this->expire)) . "'"); + } + + return true; + } + + public function destroy($session_id) { + $this->db->query("DELETE FROM `" . DB_PREFIX . "session` WHERE session_id = '" . $this->db->escape($session_id) . "'"); + + return true; + } + + public function gc($expire) { + $this->db->query("DELETE FROM `" . DB_PREFIX . "session` WHERE expire < " . ((int)time() + $expire)); + + return true; + } +} diff --git a/public/system/library/session/file.php b/public/system/library/session/file.php new file mode 100644 index 0000000..4a3b821 --- /dev/null +++ b/public/system/library/session/file.php @@ -0,0 +1,77 @@ +<?php +namespace Session; +class File { + private $directory; + + public function read($session_id) { + $file = DIR_SESSION . '/sess_' . basename($session_id); + + if (is_file($file)) { + $handle = fopen($file, 'r'); + + flock($handle, LOCK_SH); + + $data = fread($handle, filesize($file)); + + flock($handle, LOCK_UN); + + fclose($handle); + + return unserialize($data); + } else { + return array(); + } + } + + public function write($session_id, $data) { + $file = DIR_SESSION . '/sess_' . basename($session_id); + + $handle = fopen($file, 'w'); + + flock($handle, LOCK_EX); + + fwrite($handle, serialize($data)); + + fflush($handle); + + flock($handle, LOCK_UN); + + fclose($handle); + + return true; + } + + public function destroy($session_id) { + $file = DIR_SESSION . '/sess_' . basename($session_id); + + if (is_file($file)) { + unset($file); + } + } + + public function __destruct() { + if (ini_get('session.gc_divisor')) { + $gc_divisor = ini_get('session.gc_divisor'); + } else { + $gc_divisor = 1; + } + + if (ini_get('session.gc_probability')) { + $gc_probability = ini_get('session.gc_probability'); + } else { + $gc_probability = 1; + } + + if ((rand() % $gc_divisor) < $gc_probability) { + $expire = time() - ini_get('session.gc_maxlifetime'); + + $files = glob(DIR_SESSION . '/sess_*'); + + foreach ($files as $file) { + if (filemtime($file) < $expire) { + unlink($file); + } + } + } + } +}
\ No newline at end of file diff --git a/public/system/library/squareup.php b/public/system/library/squareup.php new file mode 100644 index 0000000..0eafc0b --- /dev/null +++ b/public/system/library/squareup.php @@ -0,0 +1,426 @@ +<?php + +class Squareup { + private $session; + private $url; + private $config; + private $log; + private $customer; + private $currency; + private $registry; + + const API_URL = 'https://connect.squareup.com'; + const API_VERSION = 'v2'; + const ENDPOINT_ADD_CARD = 'customers/%s/cards'; + const ENDPOINT_AUTH = 'oauth2/authorize'; + const ENDPOINT_CAPTURE_TRANSACTION = 'locations/%s/transactions/%s/capture'; + const ENDPOINT_CUSTOMERS = 'customers'; + const ENDPOINT_DELETE_CARD = 'customers/%s/cards/%s'; + const ENDPOINT_GET_TRANSACTION = 'locations/%s/transactions/%s'; + const ENDPOINT_LOCATIONS = 'locations'; + const ENDPOINT_REFRESH_TOKEN = 'oauth2/clients/%s/access-token/renew'; + const ENDPOINT_REFUND_TRANSACTION = 'locations/%s/transactions/%s/refund'; + const ENDPOINT_TOKEN = 'oauth2/token'; + const ENDPOINT_TRANSACTIONS = 'locations/%s/transactions'; + const ENDPOINT_VOID_TRANSACTION = 'locations/%s/transactions/%s/void'; + const PAYMENT_FORM_URL = 'https://js.squareup.com/v2/paymentform'; + const SCOPE = 'MERCHANT_PROFILE_READ PAYMENTS_READ SETTLEMENTS_READ CUSTOMERS_READ CUSTOMERS_WRITE'; + const VIEW_TRANSACTION_URL = 'https://squareup.com/dashboard/sales/transactions/%s/by-unit/%s'; + const SQUARE_INTEGRATION_ID = 'sqi_65a5ac54459940e3600a8561829fd970'; + + public function __construct($registry) { + $this->session = $registry->get('session'); + $this->url = $registry->get('url'); + $this->config = $registry->get('config'); + $this->log = $registry->get('log'); + $this->customer = $registry->get('customer'); + $this->currency = $registry->get('currency'); + $this->registry = $registry; + } + + public function api($request_data) { + $url = self::API_URL; + + if (empty($request_data['no_version'])) { + $url .= '/' . self::API_VERSION; + } + + $url .= '/' . $request_data['endpoint']; + + $curl_options = array( + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true + ); + + if (!empty($request_data['content_type'])) { + $content_type = $request_data['content_type']; + } else { + $content_type = 'application/json'; + } + + // handle method and parameters + if (isset($request_data['parameters']) && is_array($request_data['parameters']) && count($request_data['parameters'])) { + $params = $this->encodeParameters($request_data['parameters'], $content_type); + } else { + $params = null; + } + + switch ($request_data['method']) { + case 'GET' : + $curl_options[CURLOPT_POST] = false; + + if (is_string($params)) { + $curl_options[CURLOPT_URL] .= ((strpos($url, '?') === false) ? '?' : '&') . $params; + } + + break; + case 'POST' : + $curl_options[CURLOPT_POST] = true; + + if ($params !== null) { + $curl_options[CURLOPT_POSTFIELDS] = $params; + } + + break; + default : + $curl_options[CURLOPT_CUSTOMREQUEST] = $request_data['method']; + + if ($params !== null) { + $curl_options[CURLOPT_POSTFIELDS] = $params; + } + + break; + } + + // handle headers + $added_headers = array(); + + if (!empty($request_data['auth_type'])) { + if (empty($request_data['token'])) { + if ($this->config->get('payment_squareup_enable_sandbox')) { + $token = $this->config->get('payment_squareup_sandbox_token'); + } else { + $token = $this->config->get('payment_squareup_access_token'); + } + } else { + // custom token trumps sandbox/regular one + $token = $request_data['token']; + } + + $added_headers[] = 'Authorization: ' . $request_data['auth_type'] . ' ' . $token; + } + + if (!is_array($params)) { + // curl automatically adds Content-Type: multipart/form-data when we provide an array + $added_headers[] = 'Content-Type: ' . $content_type; + } + + if (isset($request_data['headers']) && is_array($request_data['headers'])) { + $curl_options[CURLOPT_HTTPHEADER] = array_merge($added_headers, $request_data['headers']); + } else { + $curl_options[CURLOPT_HTTPHEADER] = $added_headers; + } + + $this->debug("SQUAREUP DEBUG START..."); + $this->debug("SQUAREUP ENDPOINT: " . $curl_options[CURLOPT_URL]); + $this->debug("SQUAREUP HEADERS: " . print_r($curl_options[CURLOPT_HTTPHEADER], true)); + $this->debug("SQUAREUP PARAMS: " . $params); + + // Fire off the request + $ch = curl_init(); + curl_setopt_array($ch, $curl_options); + $result = curl_exec($ch); + + if ($result) { + $this->debug("SQUAREUP RESULT: " . $result); + + curl_close($ch); + + $return = json_decode($result, true); + + if (!empty($return['errors'])) { + throw new \Squareup\Exception($this->registry, $return['errors']); + } else { + return $return; + } + } else { + $info = curl_getinfo($ch); + + curl_close($ch); + + throw new \Squareup\Exception($this->registry, "CURL error. Info: " . print_r($info, true), true); + } + } + + public function verifyToken($access_token) { + try { + $request_data = array( + 'method' => 'GET', + 'endpoint' => self::ENDPOINT_LOCATIONS, + 'auth_type' => 'Bearer', + 'token' => $access_token + ); + + $this->api($request_data); + } catch (\Squareup\Exception $e) { + if ($e->isAccessTokenRevoked() || $e->isAccessTokenExpired()) { + return false; + } + + // In case some other error occurred + throw $e; + } + + return true; + } + + public function authLink($client_id) { + $state = $this->authState(); + + $redirect_uri = str_replace('&', '&', $this->url->link('extension/payment/squareup/oauth_callback', 'user_token=' . $this->session->data['user_token'], true)); + + $this->session->data['payment_squareup_oauth_redirect'] = $redirect_uri; + + $params = array( + 'client_id' => $client_id, + 'response_type' => 'code', + 'scope' => self::SCOPE, + 'locale' => 'en-US', + 'session' => 'false', + 'state' => $state, + 'redirect_uri' => $redirect_uri + ); + + return self::API_URL . '/' . self::ENDPOINT_AUTH . '?' . http_build_query($params); + } + + public function fetchLocations($access_token, &$first_location_id) { + $request_data = array( + 'method' => 'GET', + 'endpoint' => self::ENDPOINT_LOCATIONS, + 'auth_type' => 'Bearer', + 'token' => $access_token + ); + + $api_result = $this->api($request_data); + + $locations = array_filter($api_result['locations'], array($this, 'filterLocation')); + + if (!empty($locations)) { + $first_location = current($locations); + $first_location_id = $first_location['id']; + } else { + $first_location_id = null; + } + + return $locations; + } + + public function exchangeCodeForAccessToken($code) { + $request_data = array( + 'method' => 'POST', + 'endpoint' => self::ENDPOINT_TOKEN, + 'no_version' => true, + 'parameters' => array( + 'client_id' => $this->config->get('payment_squareup_client_id'), + 'client_secret' => $this->config->get('payment_squareup_client_secret'), + 'redirect_uri' => $this->session->data['payment_squareup_oauth_redirect'], + 'code' => $code + ) + ); + + return $this->api($request_data); + } + + public function debug($text) { + if ($this->config->get('payment_squareup_debug')) { + $this->log->write($text); + } + } + + public function refreshToken() { + $request_data = array( + 'method' => 'POST', + 'endpoint' => sprintf(self::ENDPOINT_REFRESH_TOKEN, $this->config->get('payment_squareup_client_id')), + 'no_version' => true, + 'auth_type' => 'Client', + 'token' => $this->config->get('payment_squareup_client_secret'), + 'parameters' => array( + 'access_token' => $this->config->get('payment_squareup_access_token') + ) + ); + + return $this->api($request_data); + } + + public function addCard($square_customer_id, $card_data) { + $request_data = array( + 'method' => 'POST', + 'endpoint' => sprintf(self::ENDPOINT_ADD_CARD, $square_customer_id), + 'auth_type' => 'Bearer', + 'parameters' => $card_data + ); + + $result = $this->api($request_data); + + return array( + 'id' => $result['card']['id'], + 'card_brand' => $result['card']['card_brand'], + 'last_4' => $result['card']['last_4'] + ); + } + + public function deleteCard($square_customer_id, $card) { + $request_data = array( + 'method' => 'DELETE', + 'endpoint' => sprintf(self::ENDPOINT_DELETE_CARD, $square_customer_id, $card), + 'auth_type' => 'Bearer' + ); + + return $this->api($request_data); + } + + public function addLoggedInCustomer() { + $request_data = array( + 'method' => 'POST', + 'endpoint' => self::ENDPOINT_CUSTOMERS, + 'auth_type' => 'Bearer', + 'parameters' => array( + 'given_name' => $this->customer->getFirstName(), + 'family_name' => $this->customer->getLastName(), + 'email_address' => $this->customer->getEmail(), + 'phone_number' => $this->customer->getTelephone(), + 'reference_id' => $this->customer->getId() + ) + ); + + $result = $this->api($request_data); + + return array( + 'customer_id' => $this->customer->getId(), + 'sandbox' => $this->config->get('payment_squareup_enable_sandbox'), + 'square_customer_id' => $result['customer']['id'] + ); + } + + public function addTransaction($data) { + if ($this->config->get('payment_squareup_enable_sandbox')) { + $location_id = $this->config->get('payment_squareup_sandbox_location_id'); + } else { + $location_id = $this->config->get('payment_squareup_location_id'); + } + + $request_data = array( + 'method' => 'POST', + 'endpoint' => sprintf(self::ENDPOINT_TRANSACTIONS, $location_id), + 'auth_type' => 'Bearer', + 'parameters' => $data + ); + + $result = $this->api($request_data); + + return $result['transaction']; + } + + public function getTransaction($location_id, $transaction_id) { + $request_data = array( + 'method' => 'GET', + 'endpoint' => sprintf(self::ENDPOINT_GET_TRANSACTION, $location_id, $transaction_id), + 'auth_type' => 'Bearer' + ); + + $result = $this->api($request_data); + + return $result['transaction']; + } + + public function captureTransaction($location_id, $transaction_id) { + $request_data = array( + 'method' => 'POST', + 'endpoint' => sprintf(self::ENDPOINT_CAPTURE_TRANSACTION, $location_id, $transaction_id), + 'auth_type' => 'Bearer' + ); + + $this->api($request_data); + + return $this->getTransaction($location_id, $transaction_id); + } + + public function voidTransaction($location_id, $transaction_id) { + $request_data = array( + 'method' => 'POST', + 'endpoint' => sprintf(self::ENDPOINT_VOID_TRANSACTION, $location_id, $transaction_id), + 'auth_type' => 'Bearer' + ); + + $this->api($request_data); + + return $this->getTransaction($location_id, $transaction_id); + } + + public function refundTransaction($location_id, $transaction_id, $reason, $amount, $currency, $tender_id) { + $request_data = array( + 'method' => 'POST', + 'endpoint' => sprintf(self::ENDPOINT_REFUND_TRANSACTION, $location_id, $transaction_id), + 'auth_type' => 'Bearer', + 'parameters' => array( + 'idempotency_key' => uniqid(), + 'tender_id' => $tender_id, + 'reason' => $reason, + 'amount_money' => array( + 'amount' => $this->lowestDenomination($amount, $currency), + 'currency' => $currency + ) + ) + ); + + $this->api($request_data); + + return $this->getTransaction($location_id, $transaction_id); + } + + public function lowestDenomination($value, $currency) { + $power = $this->currency->getDecimalPlace($currency); + + $value = (float)$value; + + return (int)($value * pow(10, $power)); + } + + public function standardDenomination($value, $currency) { + $power = $this->currency->getDecimalPlace($currency); + + $value = (int)$value; + + return (float)($value / pow(10, $power)); + } + + protected function filterLocation($location) { + if (empty($location['capabilities'])) { + return false; + } + + return in_array('CREDIT_CARD_PROCESSING', $location['capabilities']); + } + + protected function encodeParameters($params, $content_type) { + switch ($content_type) { + case 'application/json' : + return json_encode($params); + case 'application/x-www-form-urlencoded' : + return http_build_query($params); + default : + case 'multipart/form-data' : + // curl will handle the params as multipart form data if we just leave it as an array + return $params; + } + } + + protected function authState() { + if (!isset($this->session->data['payment_squareup_oauth_state'])) { + $this->session->data['payment_squareup_oauth_state'] = bin2hex(openssl_random_pseudo_bytes(32)); + } + + return $this->session->data['payment_squareup_oauth_state']; + } +}
\ No newline at end of file diff --git a/public/system/library/squareup/cron.php b/public/system/library/squareup/cron.php new file mode 100644 index 0000000..658f2bc --- /dev/null +++ b/public/system/library/squareup/cron.php @@ -0,0 +1,9 @@ +<?php + +$squareup_dir = dirname(__FILE__); + +require_once $squareup_dir . DIRECTORY_SEPARATOR . 'cron_functions.php'; + +if ($index = squareup_init($squareup_dir)) { + require_once $index; +}
\ No newline at end of file diff --git a/public/system/library/squareup/cron_functions.php b/public/system/library/squareup/cron_functions.php new file mode 100644 index 0000000..6a0b924 --- /dev/null +++ b/public/system/library/squareup/cron_functions.php @@ -0,0 +1,50 @@ +<?php + +function squareup_validate() { + if (php_sapi_name() != 'cli') { + die("Not in Command Line."); + } +} + +function squareup_chdir($current_dir) { + $root_dir = dirname(dirname(dirname($current_dir))); + + chdir($root_dir); + + return $root_dir; +} + +function squareup_define_route() { + define('SQUAREUP_ROUTE', 'extension/recurring/squareup/recurring'); + + $_GET['route'] = SQUAREUP_ROUTE; +} + +function squareup_init($current_dir) { + global $argc, $argv; + + // Validate environment + squareup_validate(); + + // Set up default server vars + if (isset($argc) && isset($argv) && $argc >= 3) { + $_SERVER["HTTP_HOST"] = $argv[1]; + $_SERVER["SERVER_NAME"] = $argv[1]; + $_SERVER["SERVER_PORT"] = $argv[2]; + } else { + $_SERVER["HTTP_HOST"] = "localhost"; + $_SERVER["SERVER_NAME"] = "localhost"; + $_SERVER["SERVER_PORT"] = 80; + } + + putenv("SERVER_NAME=" . $_SERVER["SERVER_NAME"]); + + // Change root dir + $root_dir = squareup_chdir($current_dir); + + squareup_define_route(); + + if (file_exists($root_dir . '/index.php')) { + return $root_dir . '/index.php'; + } +}
\ No newline at end of file diff --git a/public/system/library/squareup/exception.php b/public/system/library/squareup/exception.php new file mode 100644 index 0000000..27943cc --- /dev/null +++ b/public/system/library/squareup/exception.php @@ -0,0 +1,93 @@ +<?php + +namespace Squareup; + +class Exception extends \Exception { + const ERR_CODE_ACCESS_TOKEN_REVOKED = 'ACCESS_TOKEN_REVOKED'; + const ERR_CODE_ACCESS_TOKEN_EXPIRED = 'ACCESS_TOKEN_EXPIRED'; + + private $config; + private $log; + private $language; + private $errors; + private $isCurlError; + + private $overrideFields = array( + 'billing_address.country', + 'shipping_address.country', + 'email_address', + 'phone_number' + ); + + public function __construct($registry, $errors, $is_curl_error = false) { + $this->errors = $errors; + $this->isCurlError = $is_curl_error; + $this->config = $registry->get('config'); + $this->log = $registry->get('log'); + $this->language = $registry->get('language'); + + $message = $this->concatErrors(); + + if ($this->config->get('config_error_log')) { + $this->log->write($message); + } + + parent::__construct($message); + } + + public function isCurlError() { + return $this->isCurlError; + } + + public function isAccessTokenRevoked() { + return $this->errorCodeExists(self::ERR_CODE_ACCESS_TOKEN_REVOKED); + } + + public function isAccessTokenExpired() { + return $this->errorCodeExists(self::ERR_CODE_ACCESS_TOKEN_EXPIRED); + } + + protected function errorCodeExists($code) { + if (is_array($this->errors)) { + foreach ($this->errors as $error) { + if ($error['code'] == $code) { + return true; + } + } + } + + return false; + } + + protected function overrideError($field) { + return $this->language->get('squareup_override_error_' . $field); + } + + protected function parseError($error) { + if (!empty($error['field']) && in_array($error['field'], $this->overrideFields)) { + return $this->overrideError($error['field']); + } + + $message = $error['detail']; + + if (!empty($error['field'])) { + $message .= sprintf($this->language->get('squareup_error_field'), $error['field']); + } + + return $message; + } + + protected function concatErrors() { + $messages = array(); + + if (is_array($this->errors)) { + foreach ($this->errors as $error) { + $messages[] = $this->parseError($error); + } + } else { + $messages[] = $this->errors; + } + + return implode(' ', $messages); + } +}
\ No newline at end of file diff --git a/public/system/library/template.php b/public/system/library/template.php new file mode 100644 index 0000000..8c39209 --- /dev/null +++ b/public/system/library/template.php @@ -0,0 +1,53 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* Template class +*/ +class Template { + private $adaptor; + + /** + * Constructor + * + * @param string $adaptor + * + */ + public function __construct($adaptor) { + $class = 'Template\\' . $adaptor; + + if (class_exists($class)) { + $this->adaptor = new $class(); + } else { + throw new \Exception('Error: Could not load template adaptor ' . $adaptor . '!'); + } + } + + /** + * + * + * @param string $key + * @param mixed $value + */ + public function set($key, $value) { + $this->adaptor->set($key, $value); + } + + /** + * + * + * @param string $template + * @param bool $cache + * + * @return string + */ + public function render($template, $cache = false) { + return $this->adaptor->render($template, $cache); + } +} diff --git a/public/system/library/template/Twig/Autoloader.php b/public/system/library/template/Twig/Autoloader.php new file mode 100644 index 0000000..14016b2 --- /dev/null +++ b/public/system/library/template/Twig/Autoloader.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Autoloader class is deprecated since version 1.21 and will be removed in 2.0. Use Composer instead.', E_USER_DEPRECATED); + +/** + * Autoloads Twig classes. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.21 and will be removed in 2.0. Use Composer instead. 2.0. + */ +class Twig_Autoloader +{ + /** + * Registers Twig_Autoloader as an SPL autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not. + */ + public static function register($prepend = false) + { + @trigger_error('Using Twig_Autoloader is deprecated since version 1.21. Use Composer instead.', E_USER_DEPRECATED); + + if (PHP_VERSION_ID < 50300) { + spl_autoload_register(array(__CLASS__, 'autoload')); + } else { + spl_autoload_register(array(__CLASS__, 'autoload'), true, $prepend); + } + } + + /** + * Handles autoloading of classes. + * + * @param string $class A class name. + */ + public static function autoload($class) + { + if (0 !== strpos($class, 'Twig')) { + return; + } + + if (is_file($file = dirname(__FILE__).'/../'.str_replace(array('_', "\0"), array('/', ''), $class).'.php')) { + require $file; + } + } +} diff --git a/public/system/library/template/Twig/BaseNodeVisitor.php b/public/system/library/template/Twig/BaseNodeVisitor.php new file mode 100644 index 0000000..3c6ef66 --- /dev/null +++ b/public/system/library/template/Twig/BaseNodeVisitor.php @@ -0,0 +1,62 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Twig_BaseNodeVisitor can be used to make node visitors compatible with Twig 1.x and 2.x. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +abstract class Twig_BaseNodeVisitor implements Twig_NodeVisitorInterface +{ + /** + * {@inheritdoc} + */ + final public function enterNode(Twig_NodeInterface $node, Twig_Environment $env) + { + if (!$node instanceof Twig_Node) { + throw new LogicException('Twig_BaseNodeVisitor only supports Twig_Node instances.'); + } + + return $this->doEnterNode($node, $env); + } + + /** + * {@inheritdoc} + */ + final public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env) + { + if (!$node instanceof Twig_Node) { + throw new LogicException('Twig_BaseNodeVisitor only supports Twig_Node instances.'); + } + + return $this->doLeaveNode($node, $env); + } + + /** + * Called before child nodes are visited. + * + * @param Twig_Node $node The node to visit + * @param Twig_Environment $env The Twig environment instance + * + * @return Twig_Node The modified node + */ + abstract protected function doEnterNode(Twig_Node $node, Twig_Environment $env); + + /** + * Called after child nodes are visited. + * + * @param Twig_Node $node The node to visit + * @param Twig_Environment $env The Twig environment instance + * + * @return Twig_Node|false The modified node or false if the node must be removed + */ + abstract protected function doLeaveNode(Twig_Node $node, Twig_Environment $env); +} diff --git a/public/system/library/template/Twig/Cache/Filesystem.php b/public/system/library/template/Twig/Cache/Filesystem.php new file mode 100644 index 0000000..9e70ca9 --- /dev/null +++ b/public/system/library/template/Twig/Cache/Filesystem.php @@ -0,0 +1,98 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Implements a cache on the filesystem. + * + * @author Andrew Tch <andrew@noop.lv> + */ +class Twig_Cache_Filesystem implements Twig_CacheInterface +{ + const FORCE_BYTECODE_INVALIDATION = 1; + + private $directory; + private $options; + + /** + * @param $directory string The root cache directory + * @param $options int A set of options + */ + public function __construct($directory, $options = 0) + { + $this->directory = rtrim($directory, '\/').'/'; + $this->options = $options; + } + + /** + * {@inheritdoc} + */ + public function generateKey($name, $className) + { + $hash = hash('sha256', $className); + + return $this->directory.$hash[0].$hash[1].'/'.$hash.'.php'; + } + + /** + * {@inheritdoc} + */ + public function load($key) + { + if (file_exists($key)) { + @include_once $key; + } + } + + /** + * {@inheritdoc} + */ + public function write($key, $content) + { + $dir = dirname($key); + if (!is_dir($dir)) { + if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new RuntimeException(sprintf('Unable to create the cache directory (%s).', $dir)); + } + } elseif (!is_writable($dir)) { + throw new RuntimeException(sprintf('Unable to write in the cache directory (%s).', $dir)); + } + + $tmpFile = tempnam($dir, basename($key)); + if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $key)) { + @chmod($key, 0666 & ~umask()); + + if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) { + // Compile cached file into bytecode cache + if (function_exists('opcache_invalidate')) { + opcache_invalidate($key, true); + } elseif (function_exists('apc_compile_file')) { + apc_compile_file($key); + } + } + + return; + } + + throw new RuntimeException(sprintf('Failed to write cache file "%s".', $key)); + } + + /** + * {@inheritdoc} + */ + public function getTimestamp($key) + { + if (!file_exists($key)) { + return 0; + } + + return (int) @filemtime($key); + } +} diff --git a/public/system/library/template/Twig/Cache/Null.php b/public/system/library/template/Twig/Cache/Null.php new file mode 100644 index 0000000..fde8c80 --- /dev/null +++ b/public/system/library/template/Twig/Cache/Null.php @@ -0,0 +1,48 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Implements a no-cache strategy. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Cache_Null implements Twig_CacheInterface +{ + /** + * {@inheritdoc} + */ + public function generateKey($name, $className) + { + return ''; + } + + /** + * {@inheritdoc} + */ + public function write($key, $content) + { + } + + /** + * {@inheritdoc} + */ + public function load($key) + { + } + + /** + * {@inheritdoc} + */ + public function getTimestamp($key) + { + return 0; + } +} diff --git a/public/system/library/template/Twig/CacheInterface.php b/public/system/library/template/Twig/CacheInterface.php new file mode 100644 index 0000000..9b17e0f --- /dev/null +++ b/public/system/library/template/Twig/CacheInterface.php @@ -0,0 +1,56 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface implemented by cache classes. + * + * It is highly recommended to always store templates on the filesystem to + * benefit from the PHP opcode cache. This interface is mostly useful if you + * need to implement a custom strategy for storing templates on the filesystem. + * + * @author Andrew Tch <andrew@noop.lv> + */ +interface Twig_CacheInterface +{ + /** + * Generates a cache key for the given template class name. + * + * @param string $name The template name + * @param string $className The template class name + * + * @return string + */ + public function generateKey($name, $className); + + /** + * Writes the compiled template to cache. + * + * @param string $key The cache key + * @param string $content The template representation as a PHP class + */ + public function write($key, $content); + + /** + * Loads a template from the cache. + * + * @param string $key The cache key + */ + public function load($key); + + /** + * Returns the modification timestamp of a key. + * + * @param string $key The cache key + * + * @return int + */ + public function getTimestamp($key); +} diff --git a/public/system/library/template/Twig/Compiler.php b/public/system/library/template/Twig/Compiler.php new file mode 100644 index 0000000..abea3aa --- /dev/null +++ b/public/system/library/template/Twig/Compiler.php @@ -0,0 +1,277 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Compiles a node to PHP code. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Compiler implements Twig_CompilerInterface +{ + protected $lastLine; + protected $source; + protected $indentation; + protected $env; + protected $debugInfo = array(); + protected $sourceOffset; + protected $sourceLine; + protected $filename; + + /** + * Constructor. + * + * @param Twig_Environment $env The twig environment instance + */ + public function __construct(Twig_Environment $env) + { + $this->env = $env; + } + + public function getFilename() + { + return $this->filename; + } + + /** + * Returns the environment instance related to this compiler. + * + * @return Twig_Environment The environment instance + */ + public function getEnvironment() + { + return $this->env; + } + + /** + * Gets the current PHP code after compilation. + * + * @return string The PHP code + */ + public function getSource() + { + return $this->source; + } + + /** + * Compiles a node. + * + * @param Twig_NodeInterface $node The node to compile + * @param int $indentation The current indentation + * + * @return Twig_Compiler The current compiler instance + */ + public function compile(Twig_NodeInterface $node, $indentation = 0) + { + $this->lastLine = null; + $this->source = ''; + $this->debugInfo = array(); + $this->sourceOffset = 0; + // source code starts at 1 (as we then increment it when we encounter new lines) + $this->sourceLine = 1; + $this->indentation = $indentation; + + if ($node instanceof Twig_Node_Module) { + $this->filename = $node->getAttribute('filename'); + } + + $node->compile($this); + + return $this; + } + + public function subcompile(Twig_NodeInterface $node, $raw = true) + { + if (false === $raw) { + $this->addIndentation(); + } + + $node->compile($this); + + return $this; + } + + /** + * Adds a raw string to the compiled code. + * + * @param string $string The string + * + * @return Twig_Compiler The current compiler instance + */ + public function raw($string) + { + $this->source .= $string; + + return $this; + } + + /** + * Writes a string to the compiled code by adding indentation. + * + * @return Twig_Compiler The current compiler instance + */ + public function write() + { + $strings = func_get_args(); + foreach ($strings as $string) { + $this->addIndentation(); + $this->source .= $string; + } + + return $this; + } + + /** + * Appends an indentation to the current PHP code after compilation. + * + * @return Twig_Compiler The current compiler instance + */ + public function addIndentation() + { + $this->source .= str_repeat(' ', $this->indentation * 4); + + return $this; + } + + /** + * Adds a quoted string to the compiled code. + * + * @param string $value The string + * + * @return Twig_Compiler The current compiler instance + */ + public function string($value) + { + $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); + + return $this; + } + + /** + * Returns a PHP representation of a given value. + * + * @param mixed $value The value to convert + * + * @return Twig_Compiler The current compiler instance + */ + public function repr($value) + { + if (is_int($value) || is_float($value)) { + if (false !== $locale = setlocale(LC_NUMERIC, 0)) { + setlocale(LC_NUMERIC, 'C'); + } + + $this->raw($value); + + if (false !== $locale) { + setlocale(LC_NUMERIC, $locale); + } + } elseif (null === $value) { + $this->raw('null'); + } elseif (is_bool($value)) { + $this->raw($value ? 'true' : 'false'); + } elseif (is_array($value)) { + $this->raw('array('); + $first = true; + foreach ($value as $key => $v) { + if (!$first) { + $this->raw(', '); + } + $first = false; + $this->repr($key); + $this->raw(' => '); + $this->repr($v); + } + $this->raw(')'); + } else { + $this->string($value); + } + + return $this; + } + + /** + * Adds debugging information. + * + * @param Twig_NodeInterface $node The related twig node + * + * @return Twig_Compiler The current compiler instance + */ + public function addDebugInfo(Twig_NodeInterface $node) + { + if ($node->getLine() != $this->lastLine) { + $this->write(sprintf("// line %d\n", $node->getLine())); + + // when mbstring.func_overload is set to 2 + // mb_substr_count() replaces substr_count() + // but they have different signatures! + if (((int) ini_get('mbstring.func_overload')) & 2) { + // this is much slower than the "right" version + $this->sourceLine += mb_substr_count(mb_substr($this->source, $this->sourceOffset), "\n"); + } else { + $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset); + } + $this->sourceOffset = strlen($this->source); + $this->debugInfo[$this->sourceLine] = $node->getLine(); + + $this->lastLine = $node->getLine(); + } + + return $this; + } + + public function getDebugInfo() + { + ksort($this->debugInfo); + + return $this->debugInfo; + } + + /** + * Indents the generated code. + * + * @param int $step The number of indentation to add + * + * @return Twig_Compiler The current compiler instance + */ + public function indent($step = 1) + { + $this->indentation += $step; + + return $this; + } + + /** + * Outdents the generated code. + * + * @param int $step The number of indentation to remove + * + * @return Twig_Compiler The current compiler instance + * + * @throws LogicException When trying to outdent too much so the indentation would become negative + */ + public function outdent($step = 1) + { + // can't outdent by more steps than the current indentation level + if ($this->indentation < $step) { + throw new LogicException('Unable to call outdent() as the indentation would become negative'); + } + + $this->indentation -= $step; + + return $this; + } + + public function getVarName() + { + return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false)); + } +} diff --git a/public/system/library/template/Twig/CompilerInterface.php b/public/system/library/template/Twig/CompilerInterface.php new file mode 100644 index 0000000..272c767 --- /dev/null +++ b/public/system/library/template/Twig/CompilerInterface.php @@ -0,0 +1,36 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface implemented by compiler classes. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 3.0) + */ +interface Twig_CompilerInterface +{ + /** + * Compiles a node. + * + * @param Twig_NodeInterface $node The node to compile + * + * @return Twig_CompilerInterface The current compiler instance + */ + public function compile(Twig_NodeInterface $node); + + /** + * Gets the current PHP code after compilation. + * + * @return string The PHP code + */ + public function getSource(); +} diff --git a/public/system/library/template/Twig/Environment.php b/public/system/library/template/Twig/Environment.php new file mode 100644 index 0000000..a2e087a --- /dev/null +++ b/public/system/library/template/Twig/Environment.php @@ -0,0 +1,1418 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Stores the Twig configuration. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Environment +{ + const VERSION = '1.24.2-DEV'; + + protected $charset; + protected $loader; + protected $debug; + protected $autoReload; + protected $cache; + protected $lexer; + protected $parser; + protected $compiler; + protected $baseTemplateClass; + protected $extensions; + protected $parsers; + protected $visitors; + protected $filters; + protected $tests; + protected $functions; + protected $globals; + protected $runtimeInitialized = false; + protected $extensionInitialized = false; + protected $loadedTemplates; + protected $strictVariables; + protected $unaryOperators; + protected $binaryOperators; + protected $templateClassPrefix = '__TwigTemplate_'; + protected $functionCallbacks = array(); + protected $filterCallbacks = array(); + protected $staging; + + private $originalCache; + private $bcWriteCacheFile = false; + private $bcGetCacheFilename = false; + private $lastModifiedExtension = 0; + + /** + * Constructor. + * + * Available options: + * + * * debug: When set to true, it automatically set "auto_reload" to true as + * well (default to false). + * + * * charset: The charset used by the templates (default to UTF-8). + * + * * base_template_class: The base template class to use for generated + * templates (default to Twig_Template). + * + * * cache: An absolute path where to store the compiled templates, + * a Twig_Cache_Interface implementation, + * or false to disable compilation cache (default). + * + * * auto_reload: Whether to reload the template if the original source changed. + * If you don't provide the auto_reload option, it will be + * determined automatically based on the debug value. + * + * * strict_variables: Whether to ignore invalid variables in templates + * (default to false). + * + * * autoescape: Whether to enable auto-escaping (default to html): + * * false: disable auto-escaping + * * true: equivalent to html + * * html, js: set the autoescaping to one of the supported strategies + * * filename: set the autoescaping strategy based on the template filename extension + * * PHP callback: a PHP callback that returns an escaping strategy based on the template "filename" + * + * * optimizations: A flag that indicates which optimizations to apply + * (default to -1 which means that all optimizations are enabled; + * set it to 0 to disable). + * + * @param Twig_LoaderInterface $loader A Twig_LoaderInterface instance + * @param array $options An array of options + */ + public function __construct(Twig_LoaderInterface $loader = null, $options = array()) + { + if (null !== $loader) { + $this->setLoader($loader); + } else { + @trigger_error('Not passing a Twig_LoaderInterface as the first constructor argument of Twig_Environment is deprecated since version 1.21.', E_USER_DEPRECATED); + } + + $options = array_merge(array( + 'debug' => false, + 'charset' => 'UTF-8', + 'base_template_class' => 'Twig_Template', + 'strict_variables' => false, + 'autoescape' => 'html', + 'cache' => false, + 'auto_reload' => null, + 'optimizations' => -1, + ), $options); + + $this->debug = (bool) $options['debug']; + $this->charset = strtoupper($options['charset']); + $this->baseTemplateClass = $options['base_template_class']; + $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload']; + $this->strictVariables = (bool) $options['strict_variables']; + $this->setCache($options['cache']); + + $this->addExtension(new Twig_Extension_Core()); + $this->addExtension(new Twig_Extension_Escaper($options['autoescape'])); + $this->addExtension(new Twig_Extension_Optimizer($options['optimizations'])); + $this->staging = new Twig_Extension_Staging(); + + // For BC + if (is_string($this->originalCache)) { + $r = new ReflectionMethod($this, 'writeCacheFile'); + if ($r->getDeclaringClass()->getName() !== __CLASS__) { + @trigger_error('The Twig_Environment::writeCacheFile method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED); + + $this->bcWriteCacheFile = true; + } + + $r = new ReflectionMethod($this, 'getCacheFilename'); + if ($r->getDeclaringClass()->getName() !== __CLASS__) { + @trigger_error('The Twig_Environment::getCacheFilename method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED); + + $this->bcGetCacheFilename = true; + } + } + } + + /** + * Gets the base template class for compiled templates. + * + * @return string The base template class name + */ + public function getBaseTemplateClass() + { + return $this->baseTemplateClass; + } + + /** + * Sets the base template class for compiled templates. + * + * @param string $class The base template class name + */ + public function setBaseTemplateClass($class) + { + $this->baseTemplateClass = $class; + } + + /** + * Enables debugging mode. + */ + public function enableDebug() + { + $this->debug = true; + } + + /** + * Disables debugging mode. + */ + public function disableDebug() + { + $this->debug = false; + } + + /** + * Checks if debug mode is enabled. + * + * @return bool true if debug mode is enabled, false otherwise + */ + public function isDebug() + { + return $this->debug; + } + + /** + * Enables the auto_reload option. + */ + public function enableAutoReload() + { + $this->autoReload = true; + } + + /** + * Disables the auto_reload option. + */ + public function disableAutoReload() + { + $this->autoReload = false; + } + + /** + * Checks if the auto_reload option is enabled. + * + * @return bool true if auto_reload is enabled, false otherwise + */ + public function isAutoReload() + { + return $this->autoReload; + } + + /** + * Enables the strict_variables option. + */ + public function enableStrictVariables() + { + $this->strictVariables = true; + } + + /** + * Disables the strict_variables option. + */ + public function disableStrictVariables() + { + $this->strictVariables = false; + } + + /** + * Checks if the strict_variables option is enabled. + * + * @return bool true if strict_variables is enabled, false otherwise + */ + public function isStrictVariables() + { + return $this->strictVariables; + } + + /** + * Gets the current cache implementation. + * + * @param bool $original Whether to return the original cache option or the real cache instance + * + * @return Twig_CacheInterface|string|false A Twig_CacheInterface implementation, + * an absolute path to the compiled templates, + * or false to disable cache + */ + public function getCache($original = true) + { + return $original ? $this->originalCache : $this->cache; + } + + /** + * Sets the current cache implementation. + * + * @param Twig_CacheInterface|string|false $cache A Twig_CacheInterface implementation, + * an absolute path to the compiled templates, + * or false to disable cache + */ + public function setCache($cache) + { + if (is_string($cache)) { + $this->originalCache = $cache; + $this->cache = new Twig_Cache_Filesystem($cache); + } elseif (false === $cache) { + $this->originalCache = $cache; + $this->cache = new Twig_Cache_Null(); + } elseif (null === $cache) { + @trigger_error('Using "null" as the cache strategy is deprecated since version 1.23 and will be removed in Twig 2.0.', E_USER_DEPRECATED); + $this->originalCache = false; + $this->cache = new Twig_Cache_Null(); + } elseif ($cache instanceof Twig_CacheInterface) { + $this->originalCache = $this->cache = $cache; + } else { + throw new LogicException(sprintf('Cache can only be a string, false, or a Twig_CacheInterface implementation.')); + } + } + + /** + * Gets the cache filename for a given template. + * + * @param string $name The template name + * + * @return string|false The cache file name or false when caching is disabled + * + * @deprecated since 1.22 (to be removed in 2.0) + */ + public function getCacheFilename($name) + { + @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); + + $key = $this->cache->generateKey($name, $this->getTemplateClass($name)); + + return !$key ? false : $key; + } + + /** + * Gets the template class associated with the given string. + * + * The generated template class is based on the following parameters: + * + * * The cache key for the given template; + * * The currently enabled extensions; + * * Whether the Twig C extension is available or not. + * + * @param string $name The name for which to calculate the template class name + * @param int|null $index The index if it is an embedded template + * + * @return string The template class name + */ + public function getTemplateClass($name, $index = null) + { + $key = $this->getLoader()->getCacheKey($name); + $key .= json_encode(array_keys($this->extensions)); + $key .= function_exists('twig_template_get_attributes'); + + return $this->templateClassPrefix.hash('sha256', $key).(null === $index ? '' : '_'.$index); + } + + /** + * Gets the template class prefix. + * + * @return string The template class prefix + * + * @deprecated since 1.22 (to be removed in 2.0) + */ + public function getTemplateClassPrefix() + { + @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); + + return $this->templateClassPrefix; + } + + /** + * Renders a template. + * + * @param string $name The template name + * @param array $context An array of parameters to pass to the template + * + * @return string The rendered template + * + * @throws Twig_Error_Loader When the template cannot be found + * @throws Twig_Error_Syntax When an error occurred during compilation + * @throws Twig_Error_Runtime When an error occurred during rendering + */ + public function render($name, array $context = array()) + { + return $this->loadTemplate($name)->render($context); + } + + /** + * Displays a template. + * + * @param string $name The template name + * @param array $context An array of parameters to pass to the template + * + * @throws Twig_Error_Loader When the template cannot be found + * @throws Twig_Error_Syntax When an error occurred during compilation + * @throws Twig_Error_Runtime When an error occurred during rendering + */ + public function display($name, array $context = array()) + { + $this->loadTemplate($name)->display($context); + } + + /** + * Loads a template by name. + * + * @param string $name The template name + * @param int $index The index if it is an embedded template + * + * @return Twig_TemplateInterface A template instance representing the given template name + * + * @throws Twig_Error_Loader When the template cannot be found + * @throws Twig_Error_Syntax When an error occurred during compilation + */ + public function loadTemplate($name, $index = null) + { + $cls = $this->getTemplateClass($name, $index); + + if (isset($this->loadedTemplates[$cls])) { + return $this->loadedTemplates[$cls]; + } + + if (!class_exists($cls, false)) { + if ($this->bcGetCacheFilename) { + $key = $this->getCacheFilename($name); + } else { + $key = $this->cache->generateKey($name, $cls); + } + + if (!$this->isAutoReload() || $this->isTemplateFresh($name, $this->cache->getTimestamp($key))) { + $this->cache->load($key); + } + + if (!class_exists($cls, false)) { + $content = $this->compileSource($this->getLoader()->getSource($name), $name); + if ($this->bcWriteCacheFile) { + $this->writeCacheFile($key, $content); + } else { + $this->cache->write($key, $content); + } + + eval('?>'.$content); + } + } + + if (!$this->runtimeInitialized) { + $this->initRuntime(); + } + + return $this->loadedTemplates[$cls] = new $cls($this); + } + + /** + * Creates a template from source. + * + * This method should not be used as a generic way to load templates. + * + * @param string $template The template name + * + * @return Twig_Template A template instance representing the given template name + * + * @throws Twig_Error_Loader When the template cannot be found + * @throws Twig_Error_Syntax When an error occurred during compilation + */ + public function createTemplate($template) + { + $name = sprintf('__string_template__%s', hash('sha256', uniqid(mt_rand(), true), false)); + + $loader = new Twig_Loader_Chain(array( + new Twig_Loader_Array(array($name => $template)), + $current = $this->getLoader(), + )); + + $this->setLoader($loader); + try { + $template = $this->loadTemplate($name); + } catch (Exception $e) { + $this->setLoader($current); + + throw $e; + } catch (Throwable $e) { + $this->setLoader($current); + + throw $e; + } + $this->setLoader($current); + + return $template; + } + + /** + * Returns true if the template is still fresh. + * + * Besides checking the loader for freshness information, + * this method also checks if the enabled extensions have + * not changed. + * + * @param string $name The template name + * @param int $time The last modification time of the cached template + * + * @return bool true if the template is fresh, false otherwise + */ + public function isTemplateFresh($name, $time) + { + if (0 === $this->lastModifiedExtension) { + foreach ($this->extensions as $extension) { + $r = new ReflectionObject($extension); + if (file_exists($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModifiedExtension) { + $this->lastModifiedExtension = $extensionTime; + } + } + } + + return $this->lastModifiedExtension <= $time && $this->getLoader()->isFresh($name, $time); + } + + /** + * Tries to load a template consecutively from an array. + * + * Similar to loadTemplate() but it also accepts Twig_TemplateInterface instances and an array + * of templates where each is tried to be loaded. + * + * @param string|Twig_Template|array $names A template or an array of templates to try consecutively + * + * @return Twig_Template + * + * @throws Twig_Error_Loader When none of the templates can be found + * @throws Twig_Error_Syntax When an error occurred during compilation + */ + public function resolveTemplate($names) + { + if (!is_array($names)) { + $names = array($names); + } + + foreach ($names as $name) { + if ($name instanceof Twig_Template) { + return $name; + } + + try { + return $this->loadTemplate($name); + } catch (Twig_Error_Loader $e) { + } + } + + if (1 === count($names)) { + throw $e; + } + + throw new Twig_Error_Loader(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names))); + } + + /** + * Clears the internal template cache. + * + * @deprecated since 1.18.3 (to be removed in 2.0) + */ + public function clearTemplateCache() + { + @trigger_error(sprintf('The %s method is deprecated since version 1.18.3 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); + + $this->loadedTemplates = array(); + } + + /** + * Clears the template cache files on the filesystem. + * + * @deprecated since 1.22 (to be removed in 2.0) + */ + public function clearCacheFiles() + { + @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); + + if (is_string($this->originalCache)) { + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->originalCache), RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if ($file->isFile()) { + @unlink($file->getPathname()); + } + } + } + } + + /** + * Gets the Lexer instance. + * + * @return Twig_LexerInterface A Twig_LexerInterface instance + */ + public function getLexer() + { + if (null === $this->lexer) { + $this->lexer = new Twig_Lexer($this); + } + + return $this->lexer; + } + + /** + * Sets the Lexer instance. + * + * @param Twig_LexerInterface $lexer A Twig_LexerInterface instance + */ + public function setLexer(Twig_LexerInterface $lexer) + { + $this->lexer = $lexer; + } + + /** + * Tokenizes a source code. + * + * @param string $source The template source code + * @param string $name The template name + * + * @return Twig_TokenStream A Twig_TokenStream instance + * + * @throws Twig_Error_Syntax When the code is syntactically wrong + */ + public function tokenize($source, $name = null) + { + return $this->getLexer()->tokenize($source, $name); + } + + /** + * Gets the Parser instance. + * + * @return Twig_ParserInterface A Twig_ParserInterface instance + */ + public function getParser() + { + if (null === $this->parser) { + $this->parser = new Twig_Parser($this); + } + + return $this->parser; + } + + /** + * Sets the Parser instance. + * + * @param Twig_ParserInterface $parser A Twig_ParserInterface instance + */ + public function setParser(Twig_ParserInterface $parser) + { + $this->parser = $parser; + } + + /** + * Converts a token stream to a node tree. + * + * @param Twig_TokenStream $stream A token stream instance + * + * @return Twig_Node_Module A node tree + * + * @throws Twig_Error_Syntax When the token stream is syntactically or semantically wrong + */ + public function parse(Twig_TokenStream $stream) + { + return $this->getParser()->parse($stream); + } + + /** + * Gets the Compiler instance. + * + * @return Twig_CompilerInterface A Twig_CompilerInterface instance + */ + public function getCompiler() + { + if (null === $this->compiler) { + $this->compiler = new Twig_Compiler($this); + } + + return $this->compiler; + } + + /** + * Sets the Compiler instance. + * + * @param Twig_CompilerInterface $compiler A Twig_CompilerInterface instance + */ + public function setCompiler(Twig_CompilerInterface $compiler) + { + $this->compiler = $compiler; + } + + /** + * Compiles a node and returns the PHP code. + * + * @param Twig_NodeInterface $node A Twig_NodeInterface instance + * + * @return string The compiled PHP source code + */ + public function compile(Twig_NodeInterface $node) + { + return $this->getCompiler()->compile($node)->getSource(); + } + + /** + * Compiles a template source code. + * + * @param string $source The template source code + * @param string $name The template name + * + * @return string The compiled PHP source code + * + * @throws Twig_Error_Syntax When there was an error during tokenizing, parsing or compiling + */ + public function compileSource($source, $name = null) + { + try { + $compiled = $this->compile($this->parse($this->tokenize($source, $name)), $source); + + if (isset($source[0])) { + $compiled .= '/* '.str_replace(array('*/', "\r\n", "\r", "\n"), array('*//* ', "\n", "\n", "*/\n/* "), $source)."*/\n"; + } + + return $compiled; + } catch (Twig_Error $e) { + $e->setTemplateFile($name); + throw $e; + } catch (Exception $e) { + throw new Twig_Error_Syntax(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $name, $e); + } + } + + /** + * Sets the Loader instance. + * + * @param Twig_LoaderInterface $loader A Twig_LoaderInterface instance + */ + public function setLoader(Twig_LoaderInterface $loader) + { + $this->loader = $loader; + } + + /** + * Gets the Loader instance. + * + * @return Twig_LoaderInterface A Twig_LoaderInterface instance + */ + public function getLoader() + { + if (null === $this->loader) { + throw new LogicException('You must set a loader first.'); + } + + return $this->loader; + } + + /** + * Sets the default template charset. + * + * @param string $charset The default charset + */ + public function setCharset($charset) + { + $this->charset = strtoupper($charset); + } + + /** + * Gets the default template charset. + * + * @return string The default charset + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Initializes the runtime environment. + * + * @deprecated since 1.23 (to be removed in 2.0) + */ + public function initRuntime() + { + $this->runtimeInitialized = true; + + foreach ($this->getExtensions() as $name => $extension) { + if (!$extension instanceof Twig_Extension_InitRuntimeInterface) { + $m = new ReflectionMethod($extension, 'initRuntime'); + + if ('Twig_Extension' !== $m->getDeclaringClass()->getName()) { + @trigger_error(sprintf('Defining the initRuntime() method in the "%s" extension is deprecated since version 1.23. Use the `needs_environment` option to get the Twig_Environment instance in filters, functions, or tests; or explicitly implement Twig_Extension_InitRuntimeInterface if needed (not recommended).', $name), E_USER_DEPRECATED); + } + } + + $extension->initRuntime($this); + } + } + + /** + * Returns true if the given extension is registered. + * + * @param string $name The extension name + * + * @return bool Whether the extension is registered or not + */ + public function hasExtension($name) + { + return isset($this->extensions[$name]); + } + + /** + * Gets an extension by name. + * + * @param string $name The extension name + * + * @return Twig_ExtensionInterface A Twig_ExtensionInterface instance + */ + public function getExtension($name) + { + if (!isset($this->extensions[$name])) { + throw new Twig_Error_Runtime(sprintf('The "%s" extension is not enabled.', $name)); + } + + return $this->extensions[$name]; + } + + /** + * Registers an extension. + * + * @param Twig_ExtensionInterface $extension A Twig_ExtensionInterface instance + */ + public function addExtension(Twig_ExtensionInterface $extension) + { + $name = $extension->getName(); + + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $name)); + } + + if (isset($this->extensions[$name])) { + @trigger_error(sprintf('The possibility to register the same extension twice ("%s") is deprecated since version 1.23 and will be removed in Twig 2.0. Use proper PHP inheritance instead.', $name), E_USER_DEPRECATED); + } + + $this->lastModifiedExtension = 0; + + $this->extensions[$name] = $extension; + } + + /** + * Removes an extension by name. + * + * This method is deprecated and you should not use it. + * + * @param string $name The extension name + * + * @deprecated since 1.12 (to be removed in 2.0) + */ + public function removeExtension($name) + { + @trigger_error(sprintf('The %s method is deprecated since version 1.12 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); + + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to remove extension "%s" as extensions have already been initialized.', $name)); + } + + unset($this->extensions[$name]); + } + + /** + * Registers an array of extensions. + * + * @param array $extensions An array of extensions + */ + public function setExtensions(array $extensions) + { + foreach ($extensions as $extension) { + $this->addExtension($extension); + } + } + + /** + * Returns all registered extensions. + * + * @return array An array of extensions + */ + public function getExtensions() + { + return $this->extensions; + } + + /** + * Registers a Token Parser. + * + * @param Twig_TokenParserInterface $parser A Twig_TokenParserInterface instance + */ + public function addTokenParser(Twig_TokenParserInterface $parser) + { + if ($this->extensionInitialized) { + throw new LogicException('Unable to add a token parser as extensions have already been initialized.'); + } + + $this->staging->addTokenParser($parser); + } + + /** + * Gets the registered Token Parsers. + * + * @return Twig_TokenParserBrokerInterface A broker containing token parsers + * + * @internal + */ + public function getTokenParsers() + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + return $this->parsers; + } + + /** + * Gets registered tags. + * + * Be warned that this method cannot return tags defined by Twig_TokenParserBrokerInterface classes. + * + * @return Twig_TokenParserInterface[] An array of Twig_TokenParserInterface instances + * + * @internal + */ + public function getTags() + { + $tags = array(); + foreach ($this->getTokenParsers()->getParsers() as $parser) { + if ($parser instanceof Twig_TokenParserInterface) { + $tags[$parser->getTag()] = $parser; + } + } + + return $tags; + } + + /** + * Registers a Node Visitor. + * + * @param Twig_NodeVisitorInterface $visitor A Twig_NodeVisitorInterface instance + */ + public function addNodeVisitor(Twig_NodeVisitorInterface $visitor) + { + if ($this->extensionInitialized) { + throw new LogicException('Unable to add a node visitor as extensions have already been initialized.'); + } + + $this->staging->addNodeVisitor($visitor); + } + + /** + * Gets the registered Node Visitors. + * + * @return Twig_NodeVisitorInterface[] An array of Twig_NodeVisitorInterface instances + * + * @internal + */ + public function getNodeVisitors() + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + return $this->visitors; + } + + /** + * Registers a Filter. + * + * @param string|Twig_SimpleFilter $name The filter name or a Twig_SimpleFilter instance + * @param Twig_FilterInterface|Twig_SimpleFilter $filter A Twig_FilterInterface instance or a Twig_SimpleFilter instance + */ + public function addFilter($name, $filter = null) + { + if (!$name instanceof Twig_SimpleFilter && !($filter instanceof Twig_SimpleFilter || $filter instanceof Twig_FilterInterface)) { + throw new LogicException('A filter must be an instance of Twig_FilterInterface or Twig_SimpleFilter'); + } + + if ($name instanceof Twig_SimpleFilter) { + $filter = $name; + $name = $filter->getName(); + } else { + @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleFilter" instead when defining filter "%s".', __METHOD__, $name), E_USER_DEPRECATED); + } + + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $name)); + } + + $this->staging->addFilter($name, $filter); + } + + /** + * Get a filter by name. + * + * Subclasses may override this method and load filters differently; + * so no list of filters is available. + * + * @param string $name The filter name + * + * @return Twig_Filter|false A Twig_Filter instance or false if the filter does not exist + * + * @internal + */ + public function getFilter($name) + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + if (isset($this->filters[$name])) { + return $this->filters[$name]; + } + + foreach ($this->filters as $pattern => $filter) { + $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); + + if ($count) { + if (preg_match('#^'.$pattern.'$#', $name, $matches)) { + array_shift($matches); + $filter->setArguments($matches); + + return $filter; + } + } + } + + foreach ($this->filterCallbacks as $callback) { + if (false !== $filter = call_user_func($callback, $name)) { + return $filter; + } + } + + return false; + } + + public function registerUndefinedFilterCallback($callable) + { + $this->filterCallbacks[] = $callable; + } + + /** + * Gets the registered Filters. + * + * Be warned that this method cannot return filters defined with registerUndefinedFilterCallback. + * + * @return Twig_FilterInterface[] An array of Twig_FilterInterface instances + * + * @see registerUndefinedFilterCallback + * + * @internal + */ + public function getFilters() + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + return $this->filters; + } + + /** + * Registers a Test. + * + * @param string|Twig_SimpleTest $name The test name or a Twig_SimpleTest instance + * @param Twig_TestInterface|Twig_SimpleTest $test A Twig_TestInterface instance or a Twig_SimpleTest instance + */ + public function addTest($name, $test = null) + { + if (!$name instanceof Twig_SimpleTest && !($test instanceof Twig_SimpleTest || $test instanceof Twig_TestInterface)) { + throw new LogicException('A test must be an instance of Twig_TestInterface or Twig_SimpleTest'); + } + + if ($name instanceof Twig_SimpleTest) { + $test = $name; + $name = $test->getName(); + } else { + @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleTest" instead when defining test "%s".', __METHOD__, $name), E_USER_DEPRECATED); + } + + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $name)); + } + + $this->staging->addTest($name, $test); + } + + /** + * Gets the registered Tests. + * + * @return Twig_TestInterface[] An array of Twig_TestInterface instances + * + * @internal + */ + public function getTests() + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + return $this->tests; + } + + /** + * Gets a test by name. + * + * @param string $name The test name + * + * @return Twig_Test|false A Twig_Test instance or false if the test does not exist + * + * @internal + */ + public function getTest($name) + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + if (isset($this->tests[$name])) { + return $this->tests[$name]; + } + + return false; + } + + /** + * Registers a Function. + * + * @param string|Twig_SimpleFunction $name The function name or a Twig_SimpleFunction instance + * @param Twig_FunctionInterface|Twig_SimpleFunction $function A Twig_FunctionInterface instance or a Twig_SimpleFunction instance + */ + public function addFunction($name, $function = null) + { + if (!$name instanceof Twig_SimpleFunction && !($function instanceof Twig_SimpleFunction || $function instanceof Twig_FunctionInterface)) { + throw new LogicException('A function must be an instance of Twig_FunctionInterface or Twig_SimpleFunction'); + } + + if ($name instanceof Twig_SimpleFunction) { + $function = $name; + $name = $function->getName(); + } else { + @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleFunction" instead when defining function "%s".', __METHOD__, $name), E_USER_DEPRECATED); + } + + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $name)); + } + + $this->staging->addFunction($name, $function); + } + + /** + * Get a function by name. + * + * Subclasses may override this method and load functions differently; + * so no list of functions is available. + * + * @param string $name function name + * + * @return Twig_Function|false A Twig_Function instance or false if the function does not exist + * + * @internal + */ + public function getFunction($name) + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + if (isset($this->functions[$name])) { + return $this->functions[$name]; + } + + foreach ($this->functions as $pattern => $function) { + $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); + + if ($count) { + if (preg_match('#^'.$pattern.'$#', $name, $matches)) { + array_shift($matches); + $function->setArguments($matches); + + return $function; + } + } + } + + foreach ($this->functionCallbacks as $callback) { + if (false !== $function = call_user_func($callback, $name)) { + return $function; + } + } + + return false; + } + + public function registerUndefinedFunctionCallback($callable) + { + $this->functionCallbacks[] = $callable; + } + + /** + * Gets registered functions. + * + * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback. + * + * @return Twig_FunctionInterface[] An array of Twig_FunctionInterface instances + * + * @see registerUndefinedFunctionCallback + * + * @internal + */ + public function getFunctions() + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + return $this->functions; + } + + /** + * Registers a Global. + * + * New globals can be added before compiling or rendering a template; + * but after, you can only update existing globals. + * + * @param string $name The global name + * @param mixed $value The global value + */ + public function addGlobal($name, $value) + { + if ($this->extensionInitialized || $this->runtimeInitialized) { + if (null === $this->globals) { + $this->globals = $this->initGlobals(); + } + + if (!array_key_exists($name, $this->globals)) { + // The deprecation notice must be turned into the following exception in Twig 2.0 + @trigger_error(sprintf('Registering global variable "%s" at runtime or when the extensions have already been initialized is deprecated since version 1.21.', $name), E_USER_DEPRECATED); + //throw new LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name)); + } + } + + if ($this->extensionInitialized || $this->runtimeInitialized) { + // update the value + $this->globals[$name] = $value; + } else { + $this->staging->addGlobal($name, $value); + } + } + + /** + * Gets the registered Globals. + * + * @return array An array of globals + * + * @internal + */ + public function getGlobals() + { + if (!$this->runtimeInitialized && !$this->extensionInitialized) { + return $this->initGlobals(); + } + + if (null === $this->globals) { + $this->globals = $this->initGlobals(); + } + + return $this->globals; + } + + /** + * Merges a context with the defined globals. + * + * @param array $context An array representing the context + * + * @return array The context merged with the globals + */ + public function mergeGlobals(array $context) + { + // we don't use array_merge as the context being generally + // bigger than globals, this code is faster. + foreach ($this->getGlobals() as $key => $value) { + if (!array_key_exists($key, $context)) { + $context[$key] = $value; + } + } + + return $context; + } + + /** + * Gets the registered unary Operators. + * + * @return array An array of unary operators + * + * @internal + */ + public function getUnaryOperators() + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + return $this->unaryOperators; + } + + /** + * Gets the registered binary Operators. + * + * @return array An array of binary operators + * + * @internal + */ + public function getBinaryOperators() + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + return $this->binaryOperators; + } + + /** + * @deprecated since 1.23 (to be removed in 2.0) + */ + public function computeAlternatives($name, $items) + { + @trigger_error(sprintf('The %s method is deprecated since version 1.23 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED); + + return Twig_Error_Syntax::computeAlternatives($name, $items); + } + + /** + * @internal + */ + protected function initGlobals() + { + $globals = array(); + foreach ($this->extensions as $name => $extension) { + if (!$extension instanceof Twig_Extension_GlobalsInterface) { + $m = new ReflectionMethod($extension, 'getGlobals'); + + if ('Twig_Extension' !== $m->getDeclaringClass()->getName()) { + @trigger_error(sprintf('Defining the getGlobals() method in the "%s" extension without explicitly implementing Twig_Extension_GlobalsInterface is deprecated since version 1.23.', $name), E_USER_DEPRECATED); + } + } + + $extGlob = $extension->getGlobals(); + if (!is_array($extGlob)) { + throw new UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', get_class($extension))); + } + + $globals[] = $extGlob; + } + + $globals[] = $this->staging->getGlobals(); + + return call_user_func_array('array_merge', $globals); + } + + /** + * @internal + */ + protected function initExtensions() + { + if ($this->extensionInitialized) { + return; + } + + $this->extensionInitialized = true; + $this->parsers = new Twig_TokenParserBroker(array(), array(), false); + $this->filters = array(); + $this->functions = array(); + $this->tests = array(); + $this->visitors = array(); + $this->unaryOperators = array(); + $this->binaryOperators = array(); + + foreach ($this->extensions as $extension) { + $this->initExtension($extension); + } + $this->initExtension($this->staging); + } + + /** + * @internal + */ + protected function initExtension(Twig_ExtensionInterface $extension) + { + // filters + foreach ($extension->getFilters() as $name => $filter) { + if ($filter instanceof Twig_SimpleFilter) { + $name = $filter->getName(); + } else { + @trigger_error(sprintf('Using an instance of "%s" for filter "%s" is deprecated since version 1.21. Use Twig_SimpleFilter instead.', get_class($filter), $name), E_USER_DEPRECATED); + } + + $this->filters[$name] = $filter; + } + + // functions + foreach ($extension->getFunctions() as $name => $function) { + if ($function instanceof Twig_SimpleFunction) { + $name = $function->getName(); + } else { + @trigger_error(sprintf('Using an instance of "%s" for function "%s" is deprecated since version 1.21. Use Twig_SimpleFunction instead.', get_class($function), $name), E_USER_DEPRECATED); + } + + $this->functions[$name] = $function; + } + + // tests + foreach ($extension->getTests() as $name => $test) { + if ($test instanceof Twig_SimpleTest) { + $name = $test->getName(); + } else { + @trigger_error(sprintf('Using an instance of "%s" for test "%s" is deprecated since version 1.21. Use Twig_SimpleTest instead.', get_class($test), $name), E_USER_DEPRECATED); + } + + $this->tests[$name] = $test; + } + + // token parsers + foreach ($extension->getTokenParsers() as $parser) { + if ($parser instanceof Twig_TokenParserInterface) { + $this->parsers->addTokenParser($parser); + } elseif ($parser instanceof Twig_TokenParserBrokerInterface) { + @trigger_error('Registering a Twig_TokenParserBrokerInterface instance is deprecated since version 1.21.', E_USER_DEPRECATED); + + $this->parsers->addTokenParserBroker($parser); + } else { + throw new LogicException('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances'); + } + } + + // node visitors + foreach ($extension->getNodeVisitors() as $visitor) { + $this->visitors[] = $visitor; + } + + // operators + if ($operators = $extension->getOperators()) { + if (2 !== count($operators)) { + throw new InvalidArgumentException(sprintf('"%s::getOperators()" does not return a valid operators array.', get_class($extension))); + } + + $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]); + $this->binaryOperators = array_merge($this->binaryOperators, $operators[1]); + } + } + + /** + * @deprecated since 1.22 (to be removed in 2.0) + */ + protected function writeCacheFile($file, $content) + { + $this->cache->write($file, $content); + } +} diff --git a/public/system/library/template/Twig/Error.php b/public/system/library/template/Twig/Error.php new file mode 100644 index 0000000..37c7435 --- /dev/null +++ b/public/system/library/template/Twig/Error.php @@ -0,0 +1,272 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Twig base exception. + * + * This exception class and its children must only be used when + * an error occurs during the loading of a template, when a syntax error + * is detected in a template, or when rendering a template. Other + * errors must use regular PHP exception classes (like when the template + * cache directory is not writable for instance). + * + * To help debugging template issues, this class tracks the original template + * name and line where the error occurred. + * + * Whenever possible, you must set these information (original template name + * and line number) yourself by passing them to the constructor. If some or all + * these information are not available from where you throw the exception, then + * this class will guess them automatically (when the line number is set to -1 + * and/or the filename is set to null). As this is a costly operation, this + * can be disabled by passing false for both the filename and the line number + * when creating a new instance of this class. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Error extends Exception +{ + protected $lineno; + protected $filename; + protected $rawMessage; + protected $previous; + + /** + * Constructor. + * + * Set both the line number and the filename to false to + * disable automatic guessing of the original template name + * and line number. + * + * Set the line number to -1 to enable its automatic guessing. + * Set the filename to null to enable its automatic guessing. + * + * By default, automatic guessing is enabled. + * + * @param string $message The error message + * @param int $lineno The template line where the error occurred + * @param string $filename The template file name where the error occurred + * @param Exception $previous The previous exception + */ + public function __construct($message, $lineno = -1, $filename = null, Exception $previous = null) + { + if (PHP_VERSION_ID < 50300) { + $this->previous = $previous; + parent::__construct(''); + } else { + parent::__construct('', 0, $previous); + } + + $this->lineno = $lineno; + $this->filename = $filename; + + if (-1 === $this->lineno || null === $this->filename) { + $this->guessTemplateInfo(); + } + + $this->rawMessage = $message; + + $this->updateRepr(); + } + + /** + * Gets the raw message. + * + * @return string The raw message + */ + public function getRawMessage() + { + return $this->rawMessage; + } + + /** + * Gets the filename where the error occurred. + * + * @return string The filename + */ + public function getTemplateFile() + { + return $this->filename; + } + + /** + * Sets the filename where the error occurred. + * + * @param string $filename The filename + */ + public function setTemplateFile($filename) + { + $this->filename = $filename; + + $this->updateRepr(); + } + + /** + * Gets the template line where the error occurred. + * + * @return int The template line + */ + public function getTemplateLine() + { + return $this->lineno; + } + + /** + * Sets the template line where the error occurred. + * + * @param int $lineno The template line + */ + public function setTemplateLine($lineno) + { + $this->lineno = $lineno; + + $this->updateRepr(); + } + + public function guess() + { + $this->guessTemplateInfo(); + $this->updateRepr(); + } + + /** + * For PHP < 5.3.0, provides access to the getPrevious() method. + * + * @param string $method The method name + * @param array $arguments The parameters to be passed to the method + * + * @return Exception The previous exception or null + * + * @throws BadMethodCallException + */ + public function __call($method, $arguments) + { + if ('getprevious' == strtolower($method)) { + return $this->previous; + } + + throw new BadMethodCallException(sprintf('Method "Twig_Error::%s()" does not exist.', $method)); + } + + public function appendMessage($rawMessage) + { + $this->rawMessage .= $rawMessage; + $this->updateRepr(); + } + + /** + * @internal + */ + protected function updateRepr() + { + $this->message = $this->rawMessage; + + $dot = false; + if ('.' === substr($this->message, -1)) { + $this->message = substr($this->message, 0, -1); + $dot = true; + } + + $questionMark = false; + if ('?' === substr($this->message, -1)) { + $this->message = substr($this->message, 0, -1); + $questionMark = true; + } + + if ($this->filename) { + if (is_string($this->filename) || (is_object($this->filename) && method_exists($this->filename, '__toString'))) { + $filename = sprintf('"%s"', $this->filename); + } else { + $filename = json_encode($this->filename); + } + $this->message .= sprintf(' in %s', $filename); + } + + if ($this->lineno && $this->lineno >= 0) { + $this->message .= sprintf(' at line %d', $this->lineno); + } + + if ($dot) { + $this->message .= '.'; + } + + if ($questionMark) { + $this->message .= '?'; + } + } + + /** + * @internal + */ + protected function guessTemplateInfo() + { + $template = null; + $templateClass = null; + + if (PHP_VERSION_ID >= 50306) { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT); + } else { + $backtrace = debug_backtrace(); + } + + foreach ($backtrace as $trace) { + if (isset($trace['object']) && $trace['object'] instanceof Twig_Template && 'Twig_Template' !== get_class($trace['object'])) { + $currentClass = get_class($trace['object']); + $isEmbedContainer = 0 === strpos($templateClass, $currentClass); + if (null === $this->filename || ($this->filename == $trace['object']->getTemplateName() && !$isEmbedContainer)) { + $template = $trace['object']; + $templateClass = get_class($trace['object']); + } + } + } + + // update template filename + if (null !== $template && null === $this->filename) { + $this->filename = $template->getTemplateName(); + } + + if (null === $template || $this->lineno > -1) { + return; + } + + $r = new ReflectionObject($template); + $file = $r->getFileName(); + + // hhvm has a bug where eval'ed files comes out as the current directory + if (is_dir($file)) { + $file = ''; + } + + $exceptions = array($e = $this); + while (($e instanceof self || method_exists($e, 'getPrevious')) && $e = $e->getPrevious()) { + $exceptions[] = $e; + } + + while ($e = array_pop($exceptions)) { + $traces = $e->getTrace(); + array_unshift($traces, array('file' => $e->getFile(), 'line' => $e->getLine())); + + while ($trace = array_shift($traces)) { + if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) { + continue; + } + + foreach ($template->getDebugInfo() as $codeLine => $templateLine) { + if ($codeLine <= $trace['line']) { + // update template line + $this->lineno = $templateLine; + + return; + } + } + } + } + } +} diff --git a/public/system/library/template/Twig/Error/Loader.php b/public/system/library/template/Twig/Error/Loader.php new file mode 100644 index 0000000..68efb57 --- /dev/null +++ b/public/system/library/template/Twig/Error/Loader.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Exception thrown when an error occurs during template loading. + * + * Automatic template information guessing is always turned off as + * if a template cannot be loaded, there is nothing to guess. + * However, when a template is loaded from another one, then, we need + * to find the current context and this is automatically done by + * Twig_Template::displayWithErrorHandling(). + * + * This strategy makes Twig_Environment::resolveTemplate() much faster. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Error_Loader extends Twig_Error +{ + public function __construct($message, $lineno = -1, $filename = null, Exception $previous = null) + { + parent::__construct($message, false, false, $previous); + } +} diff --git a/public/system/library/template/Twig/Error/Runtime.php b/public/system/library/template/Twig/Error/Runtime.php new file mode 100644 index 0000000..8b6cedd --- /dev/null +++ b/public/system/library/template/Twig/Error/Runtime.php @@ -0,0 +1,20 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Exception thrown when an error occurs at runtime. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Error_Runtime extends Twig_Error +{ +} diff --git a/public/system/library/template/Twig/Error/Syntax.php b/public/system/library/template/Twig/Error/Syntax.php new file mode 100644 index 0000000..f73730a --- /dev/null +++ b/public/system/library/template/Twig/Error/Syntax.php @@ -0,0 +1,53 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Exception thrown when a syntax error occurs during lexing or parsing of a template. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Error_Syntax extends Twig_Error +{ + /** + * Tweaks the error message to include suggestions. + * + * @param string $name The original name of the item that does not exist + * @param array $items An array of possible items + */ + public function addSuggestions($name, array $items) + { + if (!$alternatives = self::computeAlternatives($name, $items)) { + return; + } + + $this->appendMessage(sprintf(' Did you mean "%s"?', implode('", "', $alternatives))); + } + + /** + * @internal + * + * To be merged with the addSuggestions() method in 2.0. + */ + public static function computeAlternatives($name, $items) + { + $alternatives = array(); + foreach ($items as $item) { + $lev = levenshtein($name, $item); + if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { + $alternatives[$item] = $lev; + } + } + asort($alternatives); + + return array_keys($alternatives); + } +} diff --git a/public/system/library/template/Twig/ExistsLoaderInterface.php b/public/system/library/template/Twig/ExistsLoaderInterface.php new file mode 100644 index 0000000..b168c3c --- /dev/null +++ b/public/system/library/template/Twig/ExistsLoaderInterface.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Adds an exists() method for loaders. + * + * @author Florin Patan <florinpatan@gmail.com> + * + * @deprecated since 1.12 (to be removed in 3.0) + */ +interface Twig_ExistsLoaderInterface +{ + /** + * Check if we have the source code of a template, given its name. + * + * @param string $name The name of the template to check if we can load + * + * @return bool If the template source code is handled by this loader or not + */ + public function exists($name); +} diff --git a/public/system/library/template/Twig/ExpressionParser.php b/public/system/library/template/Twig/ExpressionParser.php new file mode 100644 index 0000000..ec85a4d --- /dev/null +++ b/public/system/library/template/Twig/ExpressionParser.php @@ -0,0 +1,648 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Parses expressions. + * + * This parser implements a "Precedence climbing" algorithm. + * + * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm + * @see http://en.wikipedia.org/wiki/Operator-precedence_parser + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_ExpressionParser +{ + const OPERATOR_LEFT = 1; + const OPERATOR_RIGHT = 2; + + protected $parser; + protected $unaryOperators; + protected $binaryOperators; + + public function __construct(Twig_Parser $parser, array $unaryOperators, array $binaryOperators) + { + $this->parser = $parser; + $this->unaryOperators = $unaryOperators; + $this->binaryOperators = $binaryOperators; + } + + public function parseExpression($precedence = 0) + { + $expr = $this->getPrimary(); + $token = $this->parser->getCurrentToken(); + while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) { + $op = $this->binaryOperators[$token->getValue()]; + $this->parser->getStream()->next(); + + if (isset($op['callable'])) { + $expr = call_user_func($op['callable'], $this->parser, $expr); + } else { + $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); + $class = $op['class']; + $expr = new $class($expr, $expr1, $token->getLine()); + } + + $token = $this->parser->getCurrentToken(); + } + + if (0 === $precedence) { + return $this->parseConditionalExpression($expr); + } + + return $expr; + } + + protected function getPrimary() + { + $token = $this->parser->getCurrentToken(); + + if ($this->isUnary($token)) { + $operator = $this->unaryOperators[$token->getValue()]; + $this->parser->getStream()->next(); + $expr = $this->parseExpression($operator['precedence']); + $class = $operator['class']; + + return $this->parsePostfixExpression(new $class($expr, $token->getLine())); + } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '(')) { + $this->parser->getStream()->next(); + $expr = $this->parseExpression(); + $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); + + return $this->parsePostfixExpression($expr); + } + + return $this->parsePrimaryExpression(); + } + + protected function parseConditionalExpression($expr) + { + while ($this->parser->getStream()->nextIf(Twig_Token::PUNCTUATION_TYPE, '?')) { + if (!$this->parser->getStream()->nextIf(Twig_Token::PUNCTUATION_TYPE, ':')) { + $expr2 = $this->parseExpression(); + if ($this->parser->getStream()->nextIf(Twig_Token::PUNCTUATION_TYPE, ':')) { + $expr3 = $this->parseExpression(); + } else { + $expr3 = new Twig_Node_Expression_Constant('', $this->parser->getCurrentToken()->getLine()); + } + } else { + $expr2 = $expr; + $expr3 = $this->parseExpression(); + } + + $expr = new Twig_Node_Expression_Conditional($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine()); + } + + return $expr; + } + + protected function isUnary(Twig_Token $token) + { + return $token->test(Twig_Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]); + } + + protected function isBinary(Twig_Token $token) + { + return $token->test(Twig_Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]); + } + + public function parsePrimaryExpression() + { + $token = $this->parser->getCurrentToken(); + switch ($token->getType()) { + case Twig_Token::NAME_TYPE: + $this->parser->getStream()->next(); + switch ($token->getValue()) { + case 'true': + case 'TRUE': + $node = new Twig_Node_Expression_Constant(true, $token->getLine()); + break; + + case 'false': + case 'FALSE': + $node = new Twig_Node_Expression_Constant(false, $token->getLine()); + break; + + case 'none': + case 'NONE': + case 'null': + case 'NULL': + $node = new Twig_Node_Expression_Constant(null, $token->getLine()); + break; + + default: + if ('(' === $this->parser->getCurrentToken()->getValue()) { + $node = $this->getFunctionNode($token->getValue(), $token->getLine()); + } else { + $node = new Twig_Node_Expression_Name($token->getValue(), $token->getLine()); + } + } + break; + + case Twig_Token::NUMBER_TYPE: + $this->parser->getStream()->next(); + $node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); + break; + + case Twig_Token::STRING_TYPE: + case Twig_Token::INTERPOLATION_START_TYPE: + $node = $this->parseStringExpression(); + break; + + case Twig_Token::OPERATOR_TYPE: + if (preg_match(Twig_Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { + // in this context, string operators are variable names + $this->parser->getStream()->next(); + $node = new Twig_Node_Expression_Name($token->getValue(), $token->getLine()); + break; + } elseif (isset($this->unaryOperators[$token->getValue()])) { + $class = $this->unaryOperators[$token->getValue()]['class']; + + $ref = new ReflectionClass($class); + $negClass = 'Twig_Node_Expression_Unary_Neg'; + $posClass = 'Twig_Node_Expression_Unary_Pos'; + if (!(in_array($ref->getName(), array($negClass, $posClass)) || $ref->isSubclassOf($negClass) || $ref->isSubclassOf($posClass))) { + throw new Twig_Error_Syntax(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getFilename()); + } + + $this->parser->getStream()->next(); + $expr = $this->parsePrimaryExpression(); + + $node = new $class($expr, $token->getLine()); + break; + } + + default: + if ($token->test(Twig_Token::PUNCTUATION_TYPE, '[')) { + $node = $this->parseArrayExpression(); + } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) { + $node = $this->parseHashExpression(); + } else { + throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s".', Twig_Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getFilename()); + } + } + + return $this->parsePostfixExpression($node); + } + + public function parseStringExpression() + { + $stream = $this->parser->getStream(); + + $nodes = array(); + // a string cannot be followed by another string in a single expression + $nextCanBeString = true; + while (true) { + if ($nextCanBeString && $token = $stream->nextIf(Twig_Token::STRING_TYPE)) { + $nodes[] = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); + $nextCanBeString = false; + } elseif ($stream->nextIf(Twig_Token::INTERPOLATION_START_TYPE)) { + $nodes[] = $this->parseExpression(); + $stream->expect(Twig_Token::INTERPOLATION_END_TYPE); + $nextCanBeString = true; + } else { + break; + } + } + + $expr = array_shift($nodes); + foreach ($nodes as $node) { + $expr = new Twig_Node_Expression_Binary_Concat($expr, $node, $node->getLine()); + } + + return $expr; + } + + public function parseArrayExpression() + { + $stream = $this->parser->getStream(); + $stream->expect(Twig_Token::PUNCTUATION_TYPE, '[', 'An array element was expected'); + + $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine()); + $first = true; + while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) { + if (!$first) { + $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma'); + + // trailing ,? + if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) { + break; + } + } + $first = false; + + $node->addElement($this->parseExpression()); + } + $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed'); + + return $node; + } + + public function parseHashExpression() + { + $stream = $this->parser->getStream(); + $stream->expect(Twig_Token::PUNCTUATION_TYPE, '{', 'A hash element was expected'); + + $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine()); + $first = true; + while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) { + if (!$first) { + $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma'); + + // trailing ,? + if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) { + break; + } + } + $first = false; + + // a hash key can be: + // + // * a number -- 12 + // * a string -- 'a' + // * a name, which is equivalent to a string -- a + // * an expression, which must be enclosed in parentheses -- (1 + 2) + if (($token = $stream->nextIf(Twig_Token::STRING_TYPE)) || ($token = $stream->nextIf(Twig_Token::NAME_TYPE)) || $token = $stream->nextIf(Twig_Token::NUMBER_TYPE)) { + $key = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); + } elseif ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) { + $key = $this->parseExpression(); + } else { + $current = $stream->getCurrent(); + + throw new Twig_Error_Syntax(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Twig_Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $this->parser->getFilename()); + } + + $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); + $value = $this->parseExpression(); + + $node->addElement($value, $key); + } + $stream->expect(Twig_Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed'); + + return $node; + } + + public function parsePostfixExpression($node) + { + while (true) { + $token = $this->parser->getCurrentToken(); + if ($token->getType() == Twig_Token::PUNCTUATION_TYPE) { + if ('.' == $token->getValue() || '[' == $token->getValue()) { + $node = $this->parseSubscriptExpression($node); + } elseif ('|' == $token->getValue()) { + $node = $this->parseFilterExpression($node); + } else { + break; + } + } else { + break; + } + } + + return $node; + } + + public function getFunctionNode($name, $line) + { + switch ($name) { + case 'parent': + $this->parseArguments(); + if (!count($this->parser->getBlockStack())) { + throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden.', $line, $this->parser->getFilename()); + } + + if (!$this->parser->getParent() && !$this->parser->hasTraits()) { + throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getFilename()); + } + + return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line); + case 'block': + return new Twig_Node_Expression_BlockReference($this->parseArguments()->getNode(0), false, $line); + case 'attribute': + $args = $this->parseArguments(); + if (count($args) < 2) { + throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getFilename()); + } + + return new Twig_Node_Expression_GetAttr($args->getNode(0), $args->getNode(1), count($args) > 2 ? $args->getNode(2) : null, Twig_Template::ANY_CALL, $line); + default: + if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { + $arguments = new Twig_Node_Expression_Array(array(), $line); + foreach ($this->parseArguments() as $n) { + $arguments->addElement($n); + } + + $node = new Twig_Node_Expression_MethodCall($alias['node'], $alias['name'], $arguments, $line); + $node->setAttribute('safe', true); + + return $node; + } + + $args = $this->parseArguments(true); + $class = $this->getFunctionNodeClass($name, $line); + + return new $class($name, $args, $line); + } + } + + public function parseSubscriptExpression($node) + { + $stream = $this->parser->getStream(); + $token = $stream->next(); + $lineno = $token->getLine(); + $arguments = new Twig_Node_Expression_Array(array(), $lineno); + $type = Twig_Template::ANY_CALL; + if ($token->getValue() == '.') { + $token = $stream->next(); + if ( + $token->getType() == Twig_Token::NAME_TYPE + || + $token->getType() == Twig_Token::NUMBER_TYPE + || + ($token->getType() == Twig_Token::OPERATOR_TYPE && preg_match(Twig_Lexer::REGEX_NAME, $token->getValue())) + ) { + $arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno); + + if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) { + $type = Twig_Template::METHOD_CALL; + foreach ($this->parseArguments() as $n) { + $arguments->addElement($n); + } + } + } else { + throw new Twig_Error_Syntax('Expected name or number', $lineno, $this->parser->getFilename()); + } + + if ($node instanceof Twig_Node_Expression_Name && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) { + if (!$arg instanceof Twig_Node_Expression_Constant) { + throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $this->parser->getFilename()); + } + + $name = $arg->getAttribute('value'); + + if ($this->parser->isReservedMacroName($name)) { + throw new Twig_Error_Syntax(sprintf('"%s" cannot be called as macro as it is a reserved keyword.', $name), $token->getLine(), $this->parser->getFilename()); + } + + $node = new Twig_Node_Expression_MethodCall($node, 'get'.$name, $arguments, $lineno); + $node->setAttribute('safe', true); + + return $node; + } + } else { + $type = Twig_Template::ARRAY_CALL; + + // slice? + $slice = false; + if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + $arg = new Twig_Node_Expression_Constant(0, $token->getLine()); + } else { + $arg = $this->parseExpression(); + } + + if ($stream->nextIf(Twig_Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + } + + if ($slice) { + if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) { + $length = new Twig_Node_Expression_Constant(null, $token->getLine()); + } else { + $length = $this->parseExpression(); + } + + $class = $this->getFilterNodeClass('slice', $token->getLine()); + $arguments = new Twig_Node(array($arg, $length)); + $filter = new $class($node, new Twig_Node_Expression_Constant('slice', $token->getLine()), $arguments, $token->getLine()); + + $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']'); + + return $filter; + } + + $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']'); + } + + return new Twig_Node_Expression_GetAttr($node, $arg, $arguments, $type, $lineno); + } + + public function parseFilterExpression($node) + { + $this->parser->getStream()->next(); + + return $this->parseFilterExpressionRaw($node); + } + + public function parseFilterExpressionRaw($node, $tag = null) + { + while (true) { + $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE); + + $name = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); + if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) { + $arguments = new Twig_Node(); + } else { + $arguments = $this->parseArguments(true); + } + + $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine()); + + $node = new $class($node, $name, $arguments, $token->getLine(), $tag); + + if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '|')) { + break; + } + + $this->parser->getStream()->next(); + } + + return $node; + } + + /** + * Parses arguments. + * + * @param bool $namedArguments Whether to allow named arguments or not + * @param bool $definition Whether we are parsing arguments for a function definition + * + * @return Twig_Node + * + * @throws Twig_Error_Syntax + */ + public function parseArguments($namedArguments = false, $definition = false) + { + $args = array(); + $stream = $this->parser->getStream(); + + $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) { + if (!empty($args)) { + $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + } + + if ($definition) { + $token = $stream->expect(Twig_Token::NAME_TYPE, null, 'An argument must be a name'); + $value = new Twig_Node_Expression_Name($token->getValue(), $this->parser->getCurrentToken()->getLine()); + } else { + $value = $this->parseExpression(); + } + + $name = null; + if ($namedArguments && $token = $stream->nextIf(Twig_Token::OPERATOR_TYPE, '=')) { + if (!$value instanceof Twig_Node_Expression_Name) { + throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given.', get_class($value)), $token->getLine(), $this->parser->getFilename()); + } + $name = $value->getAttribute('name'); + + if ($definition) { + $value = $this->parsePrimaryExpression(); + + if (!$this->checkConstantExpression($value)) { + throw new Twig_Error_Syntax(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $this->parser->getFilename()); + } + } else { + $value = $this->parseExpression(); + } + } + + if ($definition) { + if (null === $name) { + $name = $value->getAttribute('name'); + $value = new Twig_Node_Expression_Constant(null, $this->parser->getCurrentToken()->getLine()); + } + $args[$name] = $value; + } else { + if (null === $name) { + $args[] = $value; + } else { + $args[$name] = $value; + } + } + } + $stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return new Twig_Node($args); + } + + public function parseAssignmentExpression() + { + $targets = array(); + while (true) { + $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to'); + $value = $token->getValue(); + if (in_array(strtolower($value), array('true', 'false', 'none', 'null'))) { + throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $value), $token->getLine(), $this->parser->getFilename()); + } + $targets[] = new Twig_Node_Expression_AssignName($value, $token->getLine()); + + if (!$this->parser->getStream()->nextIf(Twig_Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Twig_Node($targets); + } + + public function parseMultitargetExpression() + { + $targets = array(); + while (true) { + $targets[] = $this->parseExpression(); + if (!$this->parser->getStream()->nextIf(Twig_Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Twig_Node($targets); + } + + protected function getFunctionNodeClass($name, $line) + { + $env = $this->parser->getEnvironment(); + + if (false === $function = $env->getFunction($name)) { + $e = new Twig_Error_Syntax(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getFilename()); + $e->addSuggestions($name, array_keys($env->getFunctions())); + + throw $e; + } + + if ($function instanceof Twig_SimpleFunction && $function->isDeprecated()) { + $message = sprintf('Twig Function "%s" is deprecated', $function->getName()); + if (!is_bool($function->getDeprecatedVersion())) { + $message .= sprintf(' since version %s', $function->getDeprecatedVersion()); + } + if ($function->getAlternative()) { + $message .= sprintf('. Use "%s" instead', $function->getAlternative()); + } + $message .= sprintf(' in %s at line %d.', $this->parser->getFilename(), $line); + + @trigger_error($message, E_USER_DEPRECATED); + } + + if ($function instanceof Twig_SimpleFunction) { + return $function->getNodeClass(); + } + + return $function instanceof Twig_Function_Node ? $function->getClass() : 'Twig_Node_Expression_Function'; + } + + protected function getFilterNodeClass($name, $line) + { + $env = $this->parser->getEnvironment(); + + if (false === $filter = $env->getFilter($name)) { + $e = new Twig_Error_Syntax(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getFilename()); + $e->addSuggestions($name, array_keys($env->getFilters())); + + throw $e; + } + + if ($filter instanceof Twig_SimpleFilter && $filter->isDeprecated()) { + $message = sprintf('Twig Filter "%s" is deprecated', $filter->getName()); + if (!is_bool($filter->getDeprecatedVersion())) { + $message .= sprintf(' since version %s', $filter->getDeprecatedVersion()); + } + if ($filter->getAlternative()) { + $message .= sprintf('. Use "%s" instead', $filter->getAlternative()); + } + $message .= sprintf(' in %s at line %d.', $this->parser->getFilename(), $line); + + @trigger_error($message, E_USER_DEPRECATED); + } + + if ($filter instanceof Twig_SimpleFilter) { + return $filter->getNodeClass(); + } + + return $filter instanceof Twig_Filter_Node ? $filter->getClass() : 'Twig_Node_Expression_Filter'; + } + + // checks that the node only contains "constant" elements + protected function checkConstantExpression(Twig_NodeInterface $node) + { + if (!($node instanceof Twig_Node_Expression_Constant || $node instanceof Twig_Node_Expression_Array + || $node instanceof Twig_Node_Expression_Unary_Neg || $node instanceof Twig_Node_Expression_Unary_Pos + )) { + return false; + } + + foreach ($node as $n) { + if (!$this->checkConstantExpression($n)) { + return false; + } + } + + return true; + } +} diff --git a/public/system/library/template/Twig/Extension.php b/public/system/library/template/Twig/Extension.php new file mode 100644 index 0000000..cb03b3d --- /dev/null +++ b/public/system/library/template/Twig/Extension.php @@ -0,0 +1,79 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +abstract class Twig_Extension implements Twig_ExtensionInterface +{ + /** + * {@inheritdoc} + * + * @deprecated since 1.23 (to be removed in 2.0), implement Twig_Extension_InitRuntimeInterface instead + */ + public function initRuntime(Twig_Environment $environment) + { + } + + /** + * {@inheritdoc} + */ + public function getTokenParsers() + { + return array(); + } + + /** + * {@inheritdoc} + */ + public function getNodeVisitors() + { + return array(); + } + + /** + * {@inheritdoc} + */ + public function getFilters() + { + return array(); + } + + /** + * {@inheritdoc} + */ + public function getTests() + { + return array(); + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + return array(); + } + + /** + * {@inheritdoc} + */ + public function getOperators() + { + return array(); + } + + /** + * {@inheritdoc} + * + * @deprecated since 1.23 (to be removed in 2.0), implement Twig_Extension_GlobalsInterface instead + */ + public function getGlobals() + { + return array(); + } +} diff --git a/public/system/library/template/Twig/Extension/Core.php b/public/system/library/template/Twig/Extension/Core.php new file mode 100644 index 0000000..79ef521 --- /dev/null +++ b/public/system/library/template/Twig/Extension/Core.php @@ -0,0 +1,1548 @@ +<?php + +if (!defined('ENT_SUBSTITUTE')) { + // use 0 as hhvm does not support several flags yet + define('ENT_SUBSTITUTE', 0); +} + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Extension_Core extends Twig_Extension +{ + protected $dateFormats = array('F j, Y H:i', '%d days'); + protected $numberFormat = array(0, '.', ','); + protected $timezone = null; + protected $escapers = array(); + + /** + * Defines a new escaper to be used via the escape filter. + * + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable $callable A valid PHP callable + */ + public function setEscaper($strategy, $callable) + { + $this->escapers[$strategy] = $callable; + } + + /** + * Gets all defined escapers. + * + * @return array An array of escapers + */ + public function getEscapers() + { + return $this->escapers; + } + + /** + * Sets the default format to be used by the date filter. + * + * @param string $format The default date format string + * @param string $dateIntervalFormat The default date interval format string + */ + public function setDateFormat($format = null, $dateIntervalFormat = null) + { + if (null !== $format) { + $this->dateFormats[0] = $format; + } + + if (null !== $dateIntervalFormat) { + $this->dateFormats[1] = $dateIntervalFormat; + } + } + + /** + * Gets the default format to be used by the date filter. + * + * @return array The default date format string and the default date interval format string + */ + public function getDateFormat() + { + return $this->dateFormats; + } + + /** + * Sets the default timezone to be used by the date filter. + * + * @param DateTimeZone|string $timezone The default timezone string or a DateTimeZone object + */ + public function setTimezone($timezone) + { + $this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); + } + + /** + * Gets the default timezone to be used by the date filter. + * + * @return DateTimeZone The default timezone currently in use + */ + public function getTimezone() + { + if (null === $this->timezone) { + $this->timezone = new DateTimeZone(date_default_timezone_get()); + } + + return $this->timezone; + } + + /** + * Sets the default format to be used by the number_format filter. + * + * @param int $decimal The number of decimal places to use. + * @param string $decimalPoint The character(s) to use for the decimal point. + * @param string $thousandSep The character(s) to use for the thousands separator. + */ + public function setNumberFormat($decimal, $decimalPoint, $thousandSep) + { + $this->numberFormat = array($decimal, $decimalPoint, $thousandSep); + } + + /** + * Get the default format used by the number_format filter. + * + * @return array The arguments for number_format() + */ + public function getNumberFormat() + { + return $this->numberFormat; + } + + public function getTokenParsers() + { + return array( + new Twig_TokenParser_For(), + new Twig_TokenParser_If(), + new Twig_TokenParser_Extends(), + new Twig_TokenParser_Include(), + new Twig_TokenParser_Block(), + new Twig_TokenParser_Use(), + new Twig_TokenParser_Filter(), + new Twig_TokenParser_Macro(), + new Twig_TokenParser_Import(), + new Twig_TokenParser_From(), + new Twig_TokenParser_Set(), + new Twig_TokenParser_Spaceless(), + new Twig_TokenParser_Flush(), + new Twig_TokenParser_Do(), + new Twig_TokenParser_Embed(), + ); + } + + public function getFilters() + { + $filters = array( + // formatting filters + new Twig_SimpleFilter('date', 'twig_date_format_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('date_modify', 'twig_date_modify_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('format', 'sprintf'), + new Twig_SimpleFilter('replace', 'twig_replace_filter'), + new Twig_SimpleFilter('number_format', 'twig_number_format_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('abs', 'abs'), + new Twig_SimpleFilter('round', 'twig_round'), + + // encoding + new Twig_SimpleFilter('url_encode', 'twig_urlencode_filter'), + new Twig_SimpleFilter('json_encode', 'twig_jsonencode_filter'), + new Twig_SimpleFilter('convert_encoding', 'twig_convert_encoding'), + + // string filters + new Twig_SimpleFilter('title', 'twig_title_string_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('capitalize', 'twig_capitalize_string_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('upper', 'strtoupper'), + new Twig_SimpleFilter('lower', 'strtolower'), + new Twig_SimpleFilter('striptags', 'strip_tags'), + new Twig_SimpleFilter('trim', 'trim'), + new Twig_SimpleFilter('nl2br', 'nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))), + + // array helpers + new Twig_SimpleFilter('join', 'twig_join_filter'), + new Twig_SimpleFilter('split', 'twig_split_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('sort', 'twig_sort_filter'), + new Twig_SimpleFilter('merge', 'twig_array_merge'), + new Twig_SimpleFilter('batch', 'twig_array_batch'), + + // string/array filters + new Twig_SimpleFilter('reverse', 'twig_reverse_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('length', 'twig_length_filter', array('needs_environment' => true)), + new Twig_SimpleFilter('slice', 'twig_slice', array('needs_environment' => true)), + new Twig_SimpleFilter('first', 'twig_first', array('needs_environment' => true)), + new Twig_SimpleFilter('last', 'twig_last', array('needs_environment' => true)), + + // iteration and runtime + new Twig_SimpleFilter('default', '_twig_default_filter', array('node_class' => 'Twig_Node_Expression_Filter_Default')), + new Twig_SimpleFilter('keys', 'twig_get_array_keys_filter'), + + // escaping + new Twig_SimpleFilter('escape', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), + new Twig_SimpleFilter('e', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), + ); + + if (function_exists('mb_get_info')) { + $filters[] = new Twig_SimpleFilter('upper', 'twig_upper_filter', array('needs_environment' => true)); + $filters[] = new Twig_SimpleFilter('lower', 'twig_lower_filter', array('needs_environment' => true)); + } + + return $filters; + } + + public function getFunctions() + { + return array( + new Twig_SimpleFunction('max', 'max'), + new Twig_SimpleFunction('min', 'min'), + new Twig_SimpleFunction('range', 'range'), + new Twig_SimpleFunction('constant', 'twig_constant'), + new Twig_SimpleFunction('cycle', 'twig_cycle'), + new Twig_SimpleFunction('random', 'twig_random', array('needs_environment' => true)), + new Twig_SimpleFunction('date', 'twig_date_converter', array('needs_environment' => true)), + new Twig_SimpleFunction('include', 'twig_include', array('needs_environment' => true, 'needs_context' => true, 'is_safe' => array('all'))), + new Twig_SimpleFunction('source', 'twig_source', array('needs_environment' => true, 'is_safe' => array('all'))), + ); + } + + public function getTests() + { + return array( + new Twig_SimpleTest('even', null, array('node_class' => 'Twig_Node_Expression_Test_Even')), + new Twig_SimpleTest('odd', null, array('node_class' => 'Twig_Node_Expression_Test_Odd')), + new Twig_SimpleTest('defined', null, array('node_class' => 'Twig_Node_Expression_Test_Defined')), + new Twig_SimpleTest('sameas', null, array('node_class' => 'Twig_Node_Expression_Test_Sameas', 'deprecated' => '1.21', 'alternative' => 'same as')), + new Twig_SimpleTest('same as', null, array('node_class' => 'Twig_Node_Expression_Test_Sameas')), + new Twig_SimpleTest('none', null, array('node_class' => 'Twig_Node_Expression_Test_Null')), + new Twig_SimpleTest('null', null, array('node_class' => 'Twig_Node_Expression_Test_Null')), + new Twig_SimpleTest('divisibleby', null, array('node_class' => 'Twig_Node_Expression_Test_Divisibleby', 'deprecated' => '1.21', 'alternative' => 'divisible by')), + new Twig_SimpleTest('divisible by', null, array('node_class' => 'Twig_Node_Expression_Test_Divisibleby')), + new Twig_SimpleTest('constant', null, array('node_class' => 'Twig_Node_Expression_Test_Constant')), + new Twig_SimpleTest('empty', 'twig_test_empty'), + new Twig_SimpleTest('iterable', 'twig_test_iterable'), + ); + } + + public function getOperators() + { + return array( + array( + 'not' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'), + '-' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Neg'), + '+' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Pos'), + ), + array( + 'or' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'and' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-or' => array('precedence' => 16, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-xor' => array('precedence' => 17, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'b-and' => array('precedence' => 18, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '==' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '!=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '<' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '>' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Greater', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '>=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_GreaterEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '<=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_LessEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'not in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotIn', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_In', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'matches' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Matches', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'starts with' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_StartsWith', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'ends with' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_EndsWith', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '..' => array('precedence' => 25, 'class' => 'Twig_Node_Expression_Binary_Range', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '+' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Add', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '-' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Sub', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '~' => array('precedence' => 40, 'class' => 'Twig_Node_Expression_Binary_Concat', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '*' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mul', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '/' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Div', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '//' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_FloorDiv', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '%' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mod', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'is' => array('precedence' => 100, 'callable' => array($this, 'parseTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + 'is not' => array('precedence' => 100, 'callable' => array($this, 'parseNotTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT), + '**' => array('precedence' => 200, 'class' => 'Twig_Node_Expression_Binary_Power', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT), + '??' => array('precedence' => 300, 'class' => 'Twig_Node_Expression_NullCoalesce', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT), + ), + ); + } + + public function parseNotTestExpression(Twig_Parser $parser, Twig_NodeInterface $node) + { + return new Twig_Node_Expression_Unary_Not($this->parseTestExpression($parser, $node), $parser->getCurrentToken()->getLine()); + } + + public function parseTestExpression(Twig_Parser $parser, Twig_NodeInterface $node) + { + $stream = $parser->getStream(); + list($name, $test) = $this->getTest($parser, $node->getLine()); + + if ($test instanceof Twig_SimpleTest && $test->isDeprecated()) { + $message = sprintf('Twig Test "%s" is deprecated', $name); + if (!is_bool($test->getDeprecatedVersion())) { + $message .= sprintf(' since version %s', $test->getDeprecatedVersion()); + } + if ($test->getAlternative()) { + $message .= sprintf('. Use "%s" instead', $test->getAlternative()); + } + $message .= sprintf(' in %s at line %d.', $stream->getFilename(), $stream->getCurrent()->getLine()); + + @trigger_error($message, E_USER_DEPRECATED); + } + + $class = $this->getTestNodeClass($parser, $test); + $arguments = null; + if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) { + $arguments = $parser->getExpressionParser()->parseArguments(true); + } + + return new $class($node, $name, $arguments, $parser->getCurrentToken()->getLine()); + } + + protected function getTest(Twig_Parser $parser, $line) + { + $stream = $parser->getStream(); + $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); + $env = $parser->getEnvironment(); + + if ($test = $env->getTest($name)) { + return array($name, $test); + } + + if ($stream->test(Twig_Token::NAME_TYPE)) { + // try 2-words tests + $name = $name.' '.$parser->getCurrentToken()->getValue(); + + if ($test = $env->getTest($name)) { + $parser->getStream()->next(); + + return array($name, $test); + } + } + + $e = new Twig_Error_Syntax(sprintf('Unknown "%s" test.', $name), $line, $parser->getFilename()); + $e->addSuggestions($name, array_keys($env->getTests())); + + throw $e; + } + + protected function getTestNodeClass(Twig_Parser $parser, $test) + { + if ($test instanceof Twig_SimpleTest) { + return $test->getNodeClass(); + } + + return $test instanceof Twig_Test_Node ? $test->getClass() : 'Twig_Node_Expression_Test'; + } + + public function getName() + { + return 'core'; + } +} + +/** + * Cycles over a value. + * + * @param ArrayAccess|array $values An array or an ArrayAccess instance + * @param int $position The cycle position + * + * @return string The next value in the cycle + */ +function twig_cycle($values, $position) +{ + if (!is_array($values) && !$values instanceof ArrayAccess) { + return $values; + } + + return $values[$position % count($values)]; +} + +/** + * Returns a random value depending on the supplied parameter type: + * - a random item from a Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param Traversable|array|int|string $values The values to pick a random item from + * + * @throws Twig_Error_Runtime When $values is an empty array (does not apply to an empty string which is returned as is). + * + * @return mixed A random value from the given sequence + */ +function twig_random(Twig_Environment $env, $values = null) +{ + if (null === $values) { + return mt_rand(); + } + + if (is_int($values) || is_float($values)) { + return $values < 0 ? mt_rand($values, 0) : mt_rand(0, $values); + } + + if ($values instanceof Traversable) { + $values = iterator_to_array($values); + } elseif (is_string($values)) { + if ('' === $values) { + return ''; + } + if (null !== $charset = $env->getCharset()) { + if ('UTF-8' !== $charset) { + $values = twig_convert_encoding($values, 'UTF-8', $charset); + } + + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(?<!^)(?!$)/u', $values); + + if ('UTF-8' !== $charset) { + foreach ($values as $i => $value) { + $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); + } + } + } else { + return $values[mt_rand(0, strlen($values) - 1)]; + } + } + + if (!is_array($values)) { + return $values; + } + + if (0 === count($values)) { + throw new Twig_Error_Runtime('The random function cannot pick from an empty array.'); + } + + return $values[array_rand($values, 1)]; +} + +/** + * Converts a date to the given format. + * + * <pre> + * {{ post.published_at|date("m/d/Y") }} + * </pre> + * + * @param Twig_Environment $env A Twig_Environment instance + * @param DateTime|DateTimeInterface|DateInterval|string $date A date + * @param string|null $format The target format, null to use the default + * @param DateTimeZone|string|null|false $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return string The formatted date + */ +function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $timezone = null) +{ + if (null === $format) { + $formats = $env->getExtension('core')->getDateFormat(); + $format = $date instanceof DateInterval ? $formats[1] : $formats[0]; + } + + if ($date instanceof DateInterval) { + return $date->format($format); + } + + return twig_date_converter($env, $date, $timezone)->format($format); +} + +/** + * Returns a new date object modified. + * + * <pre> + * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} + * </pre> + * + * @param Twig_Environment $env A Twig_Environment instance + * @param DateTime|string $date A date + * @param string $modifier A modifier string + * + * @return DateTime A new date object + */ +function twig_date_modify_filter(Twig_Environment $env, $date, $modifier) +{ + $date = twig_date_converter($env, $date, false); + $resultDate = $date->modify($modifier); + + // This is a hack to ensure PHP 5.2 support and support for DateTimeImmutable + // DateTime::modify does not return the modified DateTime object < 5.3.0 + // and DateTimeImmutable does not modify $date. + return null === $resultDate ? $date : $resultDate; +} + +/** + * Converts an input to a DateTime instance. + * + * <pre> + * {% if date(user.created_at) < date('+2days') %} + * {# do something #} + * {% endif %} + * </pre> + * + * @param Twig_Environment $env A Twig_Environment instance + * @param DateTime|DateTimeInterface|string|null $date A date + * @param DateTimeZone|string|null|false $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return DateTime A DateTime instance + */ +function twig_date_converter(Twig_Environment $env, $date = null, $timezone = null) +{ + // determine the timezone + if (false !== $timezone) { + if (null === $timezone) { + $timezone = $env->getExtension('core')->getTimezone(); + } elseif (!$timezone instanceof DateTimeZone) { + $timezone = new DateTimeZone($timezone); + } + } + + // immutable dates + if ($date instanceof DateTimeImmutable) { + return false !== $timezone ? $date->setTimezone($timezone) : $date; + } + + if ($date instanceof DateTime || $date instanceof DateTimeInterface) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($timezone); + } + + return $date; + } + + if (null === $date || 'now' === $date) { + return new DateTime($date, false !== $timezone ? $timezone : $env->getExtension('core')->getTimezone()); + } + + $asString = (string) $date; + if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = new DateTime('@'.$date); + } else { + $date = new DateTime($date, $env->getExtension('core')->getTimezone()); + } + + if (false !== $timezone) { + $date->setTimezone($timezone); + } + + return $date; +} + +/** + * Replaces strings within a string. + * + * @param string $str String to replace in + * @param array|Traversable $from Replace values + * @param string|null $to Replace to, deprecated (@see http://php.net/manual/en/function.strtr.php) + * + * @return string + */ +function twig_replace_filter($str, $from, $to = null) +{ + if ($from instanceof Traversable) { + $from = iterator_to_array($from); + } elseif (is_string($from) && is_string($to)) { + @trigger_error('Using "replace" with character by character replacement is deprecated since version 1.22 and will be removed in Twig 2.0', E_USER_DEPRECATED); + + return strtr($str, $from, $to); + } elseif (!is_array($from)) { + throw new Twig_Error_Runtime(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".',is_object($from) ? get_class($from) : gettype($from))); + } + + return strtr($str, $from); +} + +/** + * Rounds a number. + * + * @param int|float $value The value to round + * @param int|float $precision The rounding precision + * @param string $method The method to use for rounding + * + * @return int|float The rounded number + */ +function twig_round($value, $precision = 0, $method = 'common') +{ + if ('common' == $method) { + return round($value, $precision); + } + + if ('ceil' != $method && 'floor' != $method) { + throw new Twig_Error_Runtime('The round filter only supports the "common", "ceil", and "floor" methods.'); + } + + return $method($value * pow(10, $precision)) / pow(10, $precision); +} + +/** + * Number format filter. + * + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $number A float/int/string of the number to format + * @param int $decimal The number of decimal points to display. + * @param string $decimalPoint The character(s) to use for the decimal point. + * @param string $thousandSep The character(s) to use for the thousands separator. + * + * @return string The formatted number + */ +function twig_number_format_filter(Twig_Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) +{ + $defaults = $env->getExtension('core')->getNumberFormat(); + if (null === $decimal) { + $decimal = $defaults[0]; + } + + if (null === $decimalPoint) { + $decimalPoint = $defaults[1]; + } + + if (null === $thousandSep) { + $thousandSep = $defaults[2]; + } + + return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); +} + +/** + * URL encodes (RFC 3986) a string as a path segment or an array as a query string. + * + * @param string|array $url A URL or an array of query parameters + * + * @return string The URL encoded value + */ +function twig_urlencode_filter($url) +{ + if (is_array($url)) { + if (defined('PHP_QUERY_RFC3986')) { + return http_build_query($url, '', '&', PHP_QUERY_RFC3986); + } + + return http_build_query($url, '', '&'); + } + + return rawurlencode($url); +} + +if (PHP_VERSION_ID < 50300) { + /** + * JSON encodes a variable. + * + * @param mixed $value The value to encode. + * @param int $options Not used on PHP 5.2.x + * + * @return mixed The JSON encoded value + */ + function twig_jsonencode_filter($value, $options = 0) + { + if ($value instanceof Twig_Markup) { + $value = (string) $value; + } elseif (is_array($value)) { + array_walk_recursive($value, '_twig_markup2string'); + } + + return json_encode($value); + } +} else { + /** + * JSON encodes a variable. + * + * @param mixed $value The value to encode. + * @param int $options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT + * + * @return mixed The JSON encoded value + */ + function twig_jsonencode_filter($value, $options = 0) + { + if ($value instanceof Twig_Markup) { + $value = (string) $value; + } elseif (is_array($value)) { + array_walk_recursive($value, '_twig_markup2string'); + } + + return json_encode($value, $options); + } +} + +function _twig_markup2string(&$value) +{ + if ($value instanceof Twig_Markup) { + $value = (string) $value; + } +} + +/** + * Merges an array with another one. + * + * <pre> + * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} + * + * {% set items = items|merge({ 'peugeot': 'car' }) %} + * + * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #} + * </pre> + * + * @param array|Traversable $arr1 An array + * @param array|Traversable $arr2 An array + * + * @return array The merged array + */ +function twig_array_merge($arr1, $arr2) +{ + if ($arr1 instanceof Traversable) { + $arr1 = iterator_to_array($arr1); + } elseif (!is_array($arr1)) { + throw new Twig_Error_Runtime(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', gettype($arr1))); + } + + if ($arr2 instanceof Traversable) { + $arr2 = iterator_to_array($arr2); + } elseif (!is_array($arr2)) { + throw new Twig_Error_Runtime(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', gettype($arr2))); + } + + return array_merge($arr1, $arr2); +} + +/** + * Slices a variable. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $item A variable + * @param int $start Start of the slice + * @param int $length Size of the slice + * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + */ +function twig_slice(Twig_Environment $env, $item, $start, $length = null, $preserveKeys = false) +{ + if ($item instanceof Traversable) { + if ($item instanceof IteratorAggregate) { + $item = $item->getIterator(); + } + + if ($start >= 0 && $length >= 0 && $item instanceof Iterator) { + try { + return iterator_to_array(new LimitIterator($item, $start, $length === null ? -1 : $length), $preserveKeys); + } catch (OutOfBoundsException $exception) { + return array(); + } + } + + $item = iterator_to_array($item, $preserveKeys); + } + + if (is_array($item)) { + return array_slice($item, $start, $length, $preserveKeys); + } + + $item = (string) $item; + + if (function_exists('mb_get_info') && null !== $charset = $env->getCharset()) { + return (string) mb_substr($item, $start, null === $length ? mb_strlen($item, $charset) - $start : $length, $charset); + } + + return (string) (null === $length ? substr($item, $start) : substr($item, $start, $length)); +} + +/** + * Returns the first element of the item. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $item A variable + * + * @return mixed The first element of the item + */ +function twig_first(Twig_Environment $env, $item) +{ + $elements = twig_slice($env, $item, 0, 1, false); + + return is_string($elements) ? $elements : current($elements); +} + +/** + * Returns the last element of the item. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $item A variable + * + * @return mixed The last element of the item + */ +function twig_last(Twig_Environment $env, $item) +{ + $elements = twig_slice($env, $item, -1, 1, false); + + return is_string($elements) ? $elements : current($elements); +} + +/** + * Joins the values to a string. + * + * The separator between elements is an empty string per default, you can define it with the optional parameter. + * + * <pre> + * {{ [1, 2, 3]|join('|') }} + * {# returns 1|2|3 #} + * + * {{ [1, 2, 3]|join }} + * {# returns 123 #} + * </pre> + * + * @param array $value An array + * @param string $glue The separator + * + * @return string The concatenated string + */ +function twig_join_filter($value, $glue = '') +{ + if ($value instanceof Traversable) { + $value = iterator_to_array($value, false); + } + + return implode($glue, (array) $value); +} + +/** + * Splits the string into an array. + * + * <pre> + * {{ "one,two,three"|split(',') }} + * {# returns [one, two, three] #} + * + * {{ "one,two,three,four,five"|split(',', 3) }} + * {# returns [one, two, "three,four,five"] #} + * + * {{ "123"|split('') }} + * {# returns [1, 2, 3] #} + * + * {{ "aabbcc"|split('', 2) }} + * {# returns [aa, bb, cc] #} + * </pre> + * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $value A string + * @param string $delimiter The delimiter + * @param int $limit The limit + * + * @return array The split string as an array + */ +function twig_split_filter(Twig_Environment $env, $value, $delimiter, $limit = null) +{ + if (!empty($delimiter)) { + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); + } + + if (!function_exists('mb_get_info') || null === $charset = $env->getCharset()) { + return str_split($value, null === $limit ? 1 : $limit); + } + + if ($limit <= 1) { + return preg_split('/(?<!^)(?!$)/u', $value); + } + + $length = mb_strlen($value, $charset); + if ($length < $limit) { + return array($value); + } + + $r = array(); + for ($i = 0; $i < $length; $i += $limit) { + $r[] = mb_substr($value, $i, $limit, $charset); + } + + return $r; +} + +// The '_default' filter is used internally to avoid using the ternary operator +// which costs a lot for big contexts (before PHP 5.4). So, on average, +// a function call is cheaper. +/** + * @internal + */ +function _twig_default_filter($value, $default = '') +{ + if (twig_test_empty($value)) { + return $default; + } + + return $value; +} + +/** + * Returns the keys for the given array. + * + * It is useful when you want to iterate over the keys of an array: + * + * <pre> + * {% for key in array|keys %} + * {# ... #} + * {% endfor %} + * </pre> + * + * @param array $array An array + * + * @return array The keys + */ +function twig_get_array_keys_filter($array) +{ + if ($array instanceof Traversable) { + return array_keys(iterator_to_array($array)); + } + + if (!is_array($array)) { + return array(); + } + + return array_keys($array); +} + +/** + * Reverses a variable. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param array|Traversable|string $item An array, a Traversable instance, or a string + * @param bool $preserveKeys Whether to preserve key or not + * + * @return mixed The reversed input + */ +function twig_reverse_filter(Twig_Environment $env, $item, $preserveKeys = false) +{ + if ($item instanceof Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); + } + + if (is_array($item)) { + return array_reverse($item, $preserveKeys); + } + + if (null !== $charset = $env->getCharset()) { + $string = (string) $item; + + if ('UTF-8' !== $charset) { + $item = twig_convert_encoding($string, 'UTF-8', $charset); + } + + preg_match_all('/./us', $item, $matches); + + $string = implode('', array_reverse($matches[0])); + + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } + + return $string; + } + + return strrev((string) $item); +} + +/** + * Sorts an array. + * + * @param array|Traversable $array + * + * @return array + */ +function twig_sort_filter($array) +{ + if ($array instanceof Traversable) { + $array = iterator_to_array($array); + } elseif (!is_array($array)) { + throw new Twig_Error_Runtime(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', gettype($array))); + } + + asort($array); + + return $array; +} + +/** + * @internal + */ +function twig_in_filter($value, $compare) +{ + if (is_array($compare)) { + return in_array($value, $compare, is_object($value) || is_resource($value)); + } elseif (is_string($compare) && (is_string($value) || is_int($value) || is_float($value))) { + return '' === $value || false !== strpos($compare, (string) $value); + } elseif ($compare instanceof Traversable) { + return in_array($value, iterator_to_array($compare, false), is_object($value) || is_resource($value)); + } + + return false; +} + +/** + * Escapes a string. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $string The value to be escaped + * @param string $strategy The escaping strategy + * @param string $charset The charset + * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) + * + * @return string + */ +function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) +{ + if ($autoescape && $string instanceof Twig_Markup) { + return $string; + } + + if (!is_string($string)) { + if (is_object($string) && method_exists($string, '__toString')) { + $string = (string) $string; + } elseif (in_array($strategy, array('html', 'js', 'css', 'html_attr', 'url'))) { + return $string; + } + } + + if (null === $charset) { + $charset = $env->getCharset(); + } + + switch ($strategy) { + case 'html': + // see http://php.net/htmlspecialchars + + // Using a static variable to avoid initializing the array + // each time the function is called. Moving the declaration on the + // top of the function slow downs other escaping strategies. + static $htmlspecialcharsCharsets; + + if (null === $htmlspecialcharsCharsets) { + if (defined('HHVM_VERSION')) { + $htmlspecialcharsCharsets = array('utf-8' => true, 'UTF-8' => true); + } else { + $htmlspecialcharsCharsets = array( + 'ISO-8859-1' => true, 'ISO8859-1' => true, + 'ISO-8859-15' => true, 'ISO8859-15' => true, + 'utf-8' => true, 'UTF-8' => true, + 'CP866' => true, 'IBM866' => true, '866' => true, + 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, + '1251' => true, + 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, + 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, + 'BIG5' => true, '950' => true, + 'GB2312' => true, '936' => true, + 'BIG5-HKSCS' => true, + 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, + 'EUC-JP' => true, 'EUCJP' => true, + 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, + ); + } + } + + if (isset($htmlspecialcharsCharsets[$charset])) { + return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); + } + + if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { + // cache the lowercase variant for future iterations + $htmlspecialcharsCharsets[$charset] = true; + + return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset); + } + + $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + + return twig_convert_encoding($string, $charset, 'UTF-8'); + + case 'js': + // escape all non-alphanumeric characters + // into their \xHH or \uHHHH representations + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } + + if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) { + throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', '_twig_escape_js_callback', $string); + + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } + + return $string; + + case 'css': + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } + + if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) { + throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', '_twig_escape_css_callback', $string); + + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } + + return $string; + + case 'html_attr': + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } + + if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) { + throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', '_twig_escape_html_attr_callback', $string); + + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } + + return $string; + + case 'url': + if (PHP_VERSION_ID < 50300) { + return str_replace('%7E', '~', rawurlencode($string)); + } + + return rawurlencode($string); + + default: + static $escapers; + + if (null === $escapers) { + $escapers = $env->getExtension('core')->getEscapers(); + } + + if (isset($escapers[$strategy])) { + return call_user_func($escapers[$strategy], $env, $string, $charset); + } + + $validStrategies = implode(', ', array_merge(array('html', 'js', 'url', 'css', 'html_attr'), array_keys($escapers))); + + throw new Twig_Error_Runtime(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies)); + } +} + +/** + * @internal + */ +function twig_escape_filter_is_safe(Twig_Node $filterArgs) +{ + foreach ($filterArgs as $arg) { + if ($arg instanceof Twig_Node_Expression_Constant) { + return array($arg->getAttribute('value')); + } + + return array(); + } + + return array('html'); +} + +if (function_exists('mb_convert_encoding')) { + function twig_convert_encoding($string, $to, $from) + { + return mb_convert_encoding($string, $to, $from); + } +} elseif (function_exists('iconv')) { + function twig_convert_encoding($string, $to, $from) + { + return iconv($from, $to, $string); + } +} else { + function twig_convert_encoding($string, $to, $from) + { + throw new Twig_Error_Runtime('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).'); + } +} + +function _twig_escape_js_callback($matches) +{ + $char = $matches[0]; + + // \xHH + if (!isset($char[1])) { + return '\\x'.strtoupper(substr('00'.bin2hex($char), -2)); + } + + // \uHHHH + $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8'); + + return '\\u'.strtoupper(substr('0000'.bin2hex($char), -4)); +} + +function _twig_escape_css_callback($matches) +{ + $char = $matches[0]; + + // \xHH + if (!isset($char[1])) { + $hex = ltrim(strtoupper(bin2hex($char)), '0'); + if (0 === strlen($hex)) { + $hex = '0'; + } + + return '\\'.$hex.' '; + } + + // \uHHHH + $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8'); + + return '\\'.ltrim(strtoupper(bin2hex($char)), '0').' '; +} + +/** + * This function is adapted from code coming from Zend Framework. + * + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +function _twig_escape_html_attr_callback($matches) +{ + /* + * While HTML supports far more named entities, the lowest common denominator + * has become HTML5's XML Serialisation which is restricted to the those named + * entities that XML supports. Using HTML entities would result in this error: + * XML Parsing Error: undefined entity + */ + static $entityMap = array( + 34 => 'quot', /* quotation mark */ + 38 => 'amp', /* ampersand */ + 60 => 'lt', /* less-than sign */ + 62 => 'gt', /* greater-than sign */ + ); + + $chr = $matches[0]; + $ord = ord($chr); + + /* + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") || ($ord >= 0x7f && $ord <= 0x9f)) { + return '�'; + } + + /* + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the hex value of the character. + */ + if (strlen($chr) == 1) { + $hex = strtoupper(substr('00'.bin2hex($chr), -2)); + } else { + $chr = twig_convert_encoding($chr, 'UTF-16BE', 'UTF-8'); + $hex = strtoupper(substr('0000'.bin2hex($chr), -4)); + } + + $int = hexdec($hex); + if (array_key_exists($int, $entityMap)) { + return sprintf('&%s;', $entityMap[$int]); + } + + /* + * Per OWASP recommendations, we'll use hex entities for any other + * characters where a named entity does not exist. + */ + return sprintf('&#x%s;', $hex); +} + +// add multibyte extensions if possible +if (function_exists('mb_get_info')) { + /** + * Returns the length of a variable. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $thing A variable + * + * @return int The length of the value + */ + function twig_length_filter(Twig_Environment $env, $thing) + { + return is_scalar($thing) ? mb_strlen($thing, $env->getCharset()) : count($thing); + } + + /** + * Converts a string to uppercase. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $string A string + * + * @return string The uppercased string + */ + function twig_upper_filter(Twig_Environment $env, $string) + { + if (null !== $charset = $env->getCharset()) { + return mb_strtoupper($string, $charset); + } + + return strtoupper($string); + } + + /** + * Converts a string to lowercase. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $string A string + * + * @return string The lowercased string + */ + function twig_lower_filter(Twig_Environment $env, $string) + { + if (null !== $charset = $env->getCharset()) { + return mb_strtolower($string, $charset); + } + + return strtolower($string); + } + + /** + * Returns a titlecased string. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $string A string + * + * @return string The titlecased string + */ + function twig_title_string_filter(Twig_Environment $env, $string) + { + if (null !== $charset = $env->getCharset()) { + return mb_convert_case($string, MB_CASE_TITLE, $charset); + } + + return ucwords(strtolower($string)); + } + + /** + * Returns a capitalized string. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $string A string + * + * @return string The capitalized string + */ + function twig_capitalize_string_filter(Twig_Environment $env, $string) + { + if (null !== $charset = $env->getCharset()) { + return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset).mb_strtolower(mb_substr($string, 1, mb_strlen($string, $charset), $charset), $charset); + } + + return ucfirst(strtolower($string)); + } +} +// and byte fallback +else { + /** + * Returns the length of a variable. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param mixed $thing A variable + * + * @return int The length of the value + */ + function twig_length_filter(Twig_Environment $env, $thing) + { + return is_scalar($thing) ? strlen($thing) : count($thing); + } + + /** + * Returns a titlecased string. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $string A string + * + * @return string The titlecased string + */ + function twig_title_string_filter(Twig_Environment $env, $string) + { + return ucwords(strtolower($string)); + } + + /** + * Returns a capitalized string. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $string A string + * + * @return string The capitalized string + */ + function twig_capitalize_string_filter(Twig_Environment $env, $string) + { + return ucfirst(strtolower($string)); + } +} + +/** + * @internal + */ +function twig_ensure_traversable($seq) +{ + if ($seq instanceof Traversable || is_array($seq)) { + return $seq; + } + + return array(); +} + +/** + * Checks if a variable is empty. + * + * <pre> + * {# evaluates to true if the foo variable is null, false, or the empty string #} + * {% if foo is empty %} + * {# ... #} + * {% endif %} + * </pre> + * + * @param mixed $value A variable + * + * @return bool true if the value is empty, false otherwise + */ +function twig_test_empty($value) +{ + if ($value instanceof Countable) { + return 0 == count($value); + } + + return '' === $value || false === $value || null === $value || array() === $value; +} + +/** + * Checks if a variable is traversable. + * + * <pre> + * {# evaluates to true if the foo variable is an array or a traversable object #} + * {% if foo is traversable %} + * {# ... #} + * {% endif %} + * </pre> + * + * @param mixed $value A variable + * + * @return bool true if the value is traversable + */ +function twig_test_iterable($value) +{ + return $value instanceof Traversable || is_array($value); +} + +/** + * Renders a template. + * + * @param Twig_Environment $env + * @param array $context + * @param string|array $template The template to render or an array of templates to try consecutively + * @param array $variables The variables to pass to the template + * @param bool $withContext + * @param bool $ignoreMissing Whether to ignore missing templates or not + * @param bool $sandboxed Whether to sandbox the template or not + * + * @return string The rendered template + */ +function twig_include(Twig_Environment $env, $context, $template, $variables = array(), $withContext = true, $ignoreMissing = false, $sandboxed = false) +{ + $alreadySandboxed = false; + $sandbox = null; + if ($withContext) { + $variables = array_merge($context, $variables); + } + + if ($isSandboxed = $sandboxed && $env->hasExtension('sandbox')) { + $sandbox = $env->getExtension('sandbox'); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); + } + } + + $result = null; + try { + $result = $env->resolveTemplate($template)->render($variables); + } catch (Twig_Error_Loader $e) { + if (!$ignoreMissing) { + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); + } + + throw $e; + } + } + + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); + } + + return $result; +} + +/** + * Returns a template content without rendering it. + * + * @param Twig_Environment $env + * @param string $name The template name + * @param bool $ignoreMissing Whether to ignore missing templates or not + * + * @return string The template source + */ +function twig_source(Twig_Environment $env, $name, $ignoreMissing = false) +{ + try { + return $env->getLoader()->getSource($name); + } catch (Twig_Error_Loader $e) { + if (!$ignoreMissing) { + throw $e; + } + } +} + +/** + * Provides the ability to get constants from instances as well as class/global constants. + * + * @param string $constant The name of the constant + * @param null|object $object The object to get the constant from + * + * @return string + */ +function twig_constant($constant, $object = null) +{ + if (null !== $object) { + $constant = get_class($object).'::'.$constant; + } + + return constant($constant); +} + +/** + * Batches item. + * + * @param array $items An array of items + * @param int $size The size of the batch + * @param mixed $fill A value used to fill missing items + * + * @return array + */ +function twig_array_batch($items, $size, $fill = null) +{ + if ($items instanceof Traversable) { + $items = iterator_to_array($items, false); + } + + $size = ceil($size); + + $result = array_chunk($items, $size, true); + + if (null !== $fill && !empty($result)) { + $last = count($result) - 1; + if ($fillCount = $size - count($result[$last])) { + $result[$last] = array_merge( + $result[$last], + array_fill(0, $fillCount, $fill) + ); + } + } + + return $result; +} diff --git a/public/system/library/template/Twig/Extension/Debug.php b/public/system/library/template/Twig/Extension/Debug.php new file mode 100644 index 0000000..42fdb1e --- /dev/null +++ b/public/system/library/template/Twig/Extension/Debug.php @@ -0,0 +1,61 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Extension_Debug extends Twig_Extension +{ + public function getFunctions() + { + // dump is safe if var_dump is overridden by xdebug + $isDumpOutputHtmlSafe = extension_loaded('xdebug') + // false means that it was not set (and the default is on) or it explicitly enabled + && (false === ini_get('xdebug.overload_var_dump') || ini_get('xdebug.overload_var_dump')) + // false means that it was not set (and the default is on) or it explicitly enabled + // xdebug.overload_var_dump produces HTML only when html_errors is also enabled + && (false === ini_get('html_errors') || ini_get('html_errors')) + || 'cli' === php_sapi_name() + ; + + return array( + new Twig_SimpleFunction('dump', 'twig_var_dump', array('is_safe' => $isDumpOutputHtmlSafe ? array('html') : array(), 'needs_context' => true, 'needs_environment' => true)), + ); + } + + public function getName() + { + return 'debug'; + } +} + +function twig_var_dump(Twig_Environment $env, $context) +{ + if (!$env->isDebug()) { + return; + } + + ob_start(); + + $count = func_num_args(); + if (2 === $count) { + $vars = array(); + foreach ($context as $key => $value) { + if (!$value instanceof Twig_Template) { + $vars[$key] = $value; + } + } + + var_dump($vars); + } else { + for ($i = 2; $i < $count; ++$i) { + var_dump(func_get_arg($i)); + } + } + + return ob_get_clean(); +} diff --git a/public/system/library/template/Twig/Extension/Escaper.php b/public/system/library/template/Twig/Extension/Escaper.php new file mode 100644 index 0000000..0be721d --- /dev/null +++ b/public/system/library/template/Twig/Extension/Escaper.php @@ -0,0 +1,102 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Extension_Escaper extends Twig_Extension +{ + protected $defaultStrategy; + + /** + * Constructor. + * + * @param string|false|callable $defaultStrategy An escaping strategy + * + * @see setDefaultStrategy() + */ + public function __construct($defaultStrategy = 'html') + { + $this->setDefaultStrategy($defaultStrategy); + } + + public function getTokenParsers() + { + return array(new Twig_TokenParser_AutoEscape()); + } + + public function getNodeVisitors() + { + return array(new Twig_NodeVisitor_Escaper()); + } + + public function getFilters() + { + return array( + new Twig_SimpleFilter('raw', 'twig_raw_filter', array('is_safe' => array('all'))), + ); + } + + /** + * Sets the default strategy to use when not defined by the user. + * + * The strategy can be a valid PHP callback that takes the template + * "filename" as an argument and returns the strategy to use. + * + * @param string|false|callable $defaultStrategy An escaping strategy + */ + public function setDefaultStrategy($defaultStrategy) + { + // for BC + if (true === $defaultStrategy) { + @trigger_error('Using "true" as the default strategy is deprecated since version 1.21. Use "html" instead.', E_USER_DEPRECATED); + + $defaultStrategy = 'html'; + } + + if ('filename' === $defaultStrategy) { + $defaultStrategy = array('Twig_FileExtensionEscapingStrategy', 'guess'); + } + + $this->defaultStrategy = $defaultStrategy; + } + + /** + * Gets the default strategy to use when not defined by the user. + * + * @param string $filename The template "filename" + * + * @return string|false The default strategy to use for the template + */ + public function getDefaultStrategy($filename) + { + // disable string callables to avoid calling a function named html or js, + // or any other upcoming escaping strategy + if (!is_string($this->defaultStrategy) && false !== $this->defaultStrategy) { + return call_user_func($this->defaultStrategy, $filename); + } + + return $this->defaultStrategy; + } + + public function getName() + { + return 'escaper'; + } +} + +/** + * Marks a variable as being safe. + * + * @param string $string A PHP variable + * + * @return string + */ +function twig_raw_filter($string) +{ + return $string; +} diff --git a/public/system/library/template/Twig/Extension/GlobalsInterface.php b/public/system/library/template/Twig/Extension/GlobalsInterface.php new file mode 100644 index 0000000..5370b8e --- /dev/null +++ b/public/system/library/template/Twig/Extension/GlobalsInterface.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Enables usage of the deprecated Twig_Extension::getGlobals() method. + * + * Explicitly implement this interface if you really need to implement the + * deprecated getGlobals() method in your extensions. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +interface Twig_Extension_GlobalsInterface +{ +} diff --git a/public/system/library/template/Twig/Extension/InitRuntimeInterface.php b/public/system/library/template/Twig/Extension/InitRuntimeInterface.php new file mode 100644 index 0000000..7a07582 --- /dev/null +++ b/public/system/library/template/Twig/Extension/InitRuntimeInterface.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Enables usage of the deprecated Twig_Extension::initRuntime() method. + * + * Explicitly implement this interface if you really need to implement the + * deprecated initRuntime() method in your extensions. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +interface Twig_Extension_InitRuntimeInterface +{ +} diff --git a/public/system/library/template/Twig/Extension/Optimizer.php b/public/system/library/template/Twig/Extension/Optimizer.php new file mode 100644 index 0000000..5a64a1a --- /dev/null +++ b/public/system/library/template/Twig/Extension/Optimizer.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Extension_Optimizer extends Twig_Extension +{ + protected $optimizers; + + public function __construct($optimizers = -1) + { + $this->optimizers = $optimizers; + } + + public function getNodeVisitors() + { + return array(new Twig_NodeVisitor_Optimizer($this->optimizers)); + } + + public function getName() + { + return 'optimizer'; + } +} diff --git a/public/system/library/template/Twig/Extension/Profiler.php b/public/system/library/template/Twig/Extension/Profiler.php new file mode 100644 index 0000000..4d9f97f --- /dev/null +++ b/public/system/library/template/Twig/Extension/Profiler.php @@ -0,0 +1,46 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +class Twig_Extension_Profiler extends Twig_Extension +{ + private $actives = array(); + + public function __construct(Twig_Profiler_Profile $profile) + { + $this->actives[] = $profile; + } + + public function enter(Twig_Profiler_Profile $profile) + { + $this->actives[0]->addProfile($profile); + array_unshift($this->actives, $profile); + } + + public function leave(Twig_Profiler_Profile $profile) + { + $profile->leave(); + array_shift($this->actives); + + if (1 === count($this->actives)) { + $this->actives[0]->leave(); + } + } + + public function getNodeVisitors() + { + return array(new Twig_Profiler_NodeVisitor_Profiler($this->getName())); + } + + public function getName() + { + return 'profiler'; + } +} diff --git a/public/system/library/template/Twig/Extension/Sandbox.php b/public/system/library/template/Twig/Extension/Sandbox.php new file mode 100644 index 0000000..760d123 --- /dev/null +++ b/public/system/library/template/Twig/Extension/Sandbox.php @@ -0,0 +1,97 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Extension_Sandbox extends Twig_Extension +{ + protected $sandboxedGlobally; + protected $sandboxed; + protected $policy; + + public function __construct(Twig_Sandbox_SecurityPolicyInterface $policy, $sandboxed = false) + { + $this->policy = $policy; + $this->sandboxedGlobally = $sandboxed; + } + + public function getTokenParsers() + { + return array(new Twig_TokenParser_Sandbox()); + } + + public function getNodeVisitors() + { + return array(new Twig_NodeVisitor_Sandbox()); + } + + public function enableSandbox() + { + $this->sandboxed = true; + } + + public function disableSandbox() + { + $this->sandboxed = false; + } + + public function isSandboxed() + { + return $this->sandboxedGlobally || $this->sandboxed; + } + + public function isSandboxedGlobally() + { + return $this->sandboxedGlobally; + } + + public function setSecurityPolicy(Twig_Sandbox_SecurityPolicyInterface $policy) + { + $this->policy = $policy; + } + + public function getSecurityPolicy() + { + return $this->policy; + } + + public function checkSecurity($tags, $filters, $functions) + { + if ($this->isSandboxed()) { + $this->policy->checkSecurity($tags, $filters, $functions); + } + } + + public function checkMethodAllowed($obj, $method) + { + if ($this->isSandboxed()) { + $this->policy->checkMethodAllowed($obj, $method); + } + } + + public function checkPropertyAllowed($obj, $method) + { + if ($this->isSandboxed()) { + $this->policy->checkPropertyAllowed($obj, $method); + } + } + + public function ensureToStringAllowed($obj) + { + if ($this->isSandboxed() && is_object($obj)) { + $this->policy->checkMethodAllowed($obj, '__toString'); + } + + return $obj; + } + + public function getName() + { + return 'sandbox'; + } +} diff --git a/public/system/library/template/Twig/Extension/Staging.php b/public/system/library/template/Twig/Extension/Staging.php new file mode 100644 index 0000000..d21004d --- /dev/null +++ b/public/system/library/template/Twig/Extension/Staging.php @@ -0,0 +1,94 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Internal class. + * + * This class is used by Twig_Environment as a staging area and must not be used directly. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @internal + */ +class Twig_Extension_Staging extends Twig_Extension +{ + protected $functions = array(); + protected $filters = array(); + protected $visitors = array(); + protected $tokenParsers = array(); + protected $globals = array(); + protected $tests = array(); + + public function addFunction($name, $function) + { + $this->functions[$name] = $function; + } + + public function getFunctions() + { + return $this->functions; + } + + public function addFilter($name, $filter) + { + $this->filters[$name] = $filter; + } + + public function getFilters() + { + return $this->filters; + } + + public function addNodeVisitor(Twig_NodeVisitorInterface $visitor) + { + $this->visitors[] = $visitor; + } + + public function getNodeVisitors() + { + return $this->visitors; + } + + public function addTokenParser(Twig_TokenParserInterface $parser) + { + $this->tokenParsers[] = $parser; + } + + public function getTokenParsers() + { + return $this->tokenParsers; + } + + public function addGlobal($name, $value) + { + $this->globals[$name] = $value; + } + + public function getGlobals() + { + return $this->globals; + } + + public function addTest($name, $test) + { + $this->tests[$name] = $test; + } + + public function getTests() + { + return $this->tests; + } + + public function getName() + { + return 'staging'; + } +} diff --git a/public/system/library/template/Twig/Extension/StringLoader.php b/public/system/library/template/Twig/Extension/StringLoader.php new file mode 100644 index 0000000..2a3ddb6 --- /dev/null +++ b/public/system/library/template/Twig/Extension/StringLoader.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Extension_StringLoader extends Twig_Extension +{ + public function getFunctions() + { + return array( + new Twig_SimpleFunction('template_from_string', 'twig_template_from_string', array('needs_environment' => true)), + ); + } + + public function getName() + { + return 'string_loader'; + } +} + +/** + * Loads a template from a string. + * + * <pre> + * {{ include(template_from_string("Hello {{ name }}")) }} + * </pre> + * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $template A template as a string or object implementing __toString() + * + * @return Twig_Template A Twig_Template instance + */ +function twig_template_from_string(Twig_Environment $env, $template) +{ + return $env->createTemplate((string) $template); +} diff --git a/public/system/library/template/Twig/ExtensionInterface.php b/public/system/library/template/Twig/ExtensionInterface.php new file mode 100644 index 0000000..776ffeb --- /dev/null +++ b/public/system/library/template/Twig/ExtensionInterface.php @@ -0,0 +1,87 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface implemented by extension classes. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +interface Twig_ExtensionInterface +{ + /** + * Initializes the runtime environment. + * + * This is where you can load some file that contains filter functions for instance. + * + * @param Twig_Environment $environment The current Twig_Environment instance + * + * @deprecated since 1.23 (to be removed in 2.0), implement Twig_Extension_InitRuntimeInterface instead + */ + public function initRuntime(Twig_Environment $environment); + + /** + * Returns the token parser instances to add to the existing list. + * + * @return Twig_TokenParserInterface[] + */ + public function getTokenParsers(); + + /** + * Returns the node visitor instances to add to the existing list. + * + * @return Twig_NodeVisitorInterface[] An array of Twig_NodeVisitorInterface instances + */ + public function getNodeVisitors(); + + /** + * Returns a list of filters to add to the existing list. + * + * @return Twig_SimpleFilter[] + */ + public function getFilters(); + + /** + * Returns a list of tests to add to the existing list. + * + * @return Twig_SimpleTest[] + */ + public function getTests(); + + /** + * Returns a list of functions to add to the existing list. + * + * @return Twig_SimpleFunction[] + */ + public function getFunctions(); + + /** + * Returns a list of operators to add to the existing list. + * + * @return array An array of operators + */ + public function getOperators(); + + /** + * Returns a list of global variables to add to the existing list. + * + * @return array An array of global variables + * + * @deprecated since 1.23 (to be removed in 2.0), implement Twig_Extension_GlobalsInterface instead + */ + public function getGlobals(); + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName(); +} diff --git a/public/system/library/template/Twig/FileExtensionEscapingStrategy.php b/public/system/library/template/Twig/FileExtensionEscapingStrategy.php new file mode 100644 index 0000000..9bda0b4 --- /dev/null +++ b/public/system/library/template/Twig/FileExtensionEscapingStrategy.php @@ -0,0 +1,58 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Default autoescaping strategy based on file names. + * + * This strategy sets the HTML as the default autoescaping strategy, + * but changes it based on the filename. + * + * Note that there is no runtime performance impact as the + * default autoescaping strategy is set at compilation time. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_FileExtensionEscapingStrategy +{ + /** + * Guesses the best autoescaping strategy based on the file name. + * + * @param string $filename The template file name + * + * @return string|false The escaping strategy name to use or false to disable + */ + public static function guess($filename) + { + if (in_array(substr($filename, -1), array('/', '\\'))) { + return 'html'; // return html for directories + } + + if ('.twig' === substr($filename, -5)) { + $filename = substr($filename, 0, -5); + } + + $extension = pathinfo($filename, PATHINFO_EXTENSION); + + switch ($extension) { + case 'js': + return 'js'; + + case 'css': + return 'css'; + + case 'txt': + return false; + + default: + return 'html'; + } + } +} diff --git a/public/system/library/template/Twig/Filter.php b/public/system/library/template/Twig/Filter.php new file mode 100644 index 0000000..101d2e7 --- /dev/null +++ b/public/system/library/template/Twig/Filter.php @@ -0,0 +1,84 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Filter class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleFilter instead.', E_USER_DEPRECATED); + +/** + * Represents a template filter. + * + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +abstract class Twig_Filter implements Twig_FilterInterface, Twig_FilterCallableInterface +{ + protected $options; + protected $arguments = array(); + + public function __construct(array $options = array()) + { + $this->options = array_merge(array( + 'needs_environment' => false, + 'needs_context' => false, + 'pre_escape' => null, + 'preserves_safety' => null, + 'callable' => null, + ), $options); + } + + public function setArguments($arguments) + { + $this->arguments = $arguments; + } + + public function getArguments() + { + return $this->arguments; + } + + public function needsEnvironment() + { + return $this->options['needs_environment']; + } + + public function needsContext() + { + return $this->options['needs_context']; + } + + public function getSafe(Twig_Node $filterArgs) + { + if (isset($this->options['is_safe'])) { + return $this->options['is_safe']; + } + + if (isset($this->options['is_safe_callback'])) { + return call_user_func($this->options['is_safe_callback'], $filterArgs); + } + } + + public function getPreservesSafety() + { + return $this->options['preserves_safety']; + } + + public function getPreEscape() + { + return $this->options['pre_escape']; + } + + public function getCallable() + { + return $this->options['callable']; + } +} diff --git a/public/system/library/template/Twig/Filter/Function.php b/public/system/library/template/Twig/Filter/Function.php new file mode 100644 index 0000000..d679cab --- /dev/null +++ b/public/system/library/template/Twig/Filter/Function.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Filter_Function class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleFilter instead.', E_USER_DEPRECATED); + +/** + * Represents a function template filter. + * + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +class Twig_Filter_Function extends Twig_Filter +{ + protected $function; + + public function __construct($function, array $options = array()) + { + $options['callable'] = $function; + + parent::__construct($options); + + $this->function = $function; + } + + public function compile() + { + return $this->function; + } +} diff --git a/public/system/library/template/Twig/Filter/Method.php b/public/system/library/template/Twig/Filter/Method.php new file mode 100644 index 0000000..655aab4 --- /dev/null +++ b/public/system/library/template/Twig/Filter/Method.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Filter_Method class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleFilter instead.', E_USER_DEPRECATED); + +/** + * Represents a method template filter. + * + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +class Twig_Filter_Method extends Twig_Filter +{ + protected $extension; + protected $method; + + public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array()) + { + $options['callable'] = array($extension, $method); + + parent::__construct($options); + + $this->extension = $extension; + $this->method = $method; + } + + public function compile() + { + return sprintf('$this->env->getExtension(\'%s\')->%s', $this->extension->getName(), $this->method); + } +} diff --git a/public/system/library/template/Twig/Filter/Node.php b/public/system/library/template/Twig/Filter/Node.php new file mode 100644 index 0000000..a922f50 --- /dev/null +++ b/public/system/library/template/Twig/Filter/Node.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Filter_Node class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleFilter instead.', E_USER_DEPRECATED); + +/** + * Represents a template filter as a node. + * + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +class Twig_Filter_Node extends Twig_Filter +{ + protected $class; + + public function __construct($class, array $options = array()) + { + parent::__construct($options); + + $this->class = $class; + } + + public function getClass() + { + return $this->class; + } + + public function compile() + { + } +} diff --git a/public/system/library/template/Twig/FilterCallableInterface.php b/public/system/library/template/Twig/FilterCallableInterface.php new file mode 100644 index 0000000..5679861 --- /dev/null +++ b/public/system/library/template/Twig/FilterCallableInterface.php @@ -0,0 +1,24 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a callable template filter. + * + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_FilterCallableInterface +{ + public function getCallable(); +} diff --git a/public/system/library/template/Twig/FilterInterface.php b/public/system/library/template/Twig/FilterInterface.php new file mode 100644 index 0000000..6b0be0e --- /dev/null +++ b/public/system/library/template/Twig/FilterInterface.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a template filter. + * + * Use Twig_SimpleFilter instead. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_FilterInterface +{ + /** + * Compiles a filter. + * + * @return string The PHP code for the filter + */ + public function compile(); + + public function needsEnvironment(); + + public function needsContext(); + + public function getSafe(Twig_Node $filterArgs); + + public function getPreservesSafety(); + + public function getPreEscape(); + + public function setArguments($arguments); + + public function getArguments(); +} diff --git a/public/system/library/template/Twig/Function.php b/public/system/library/template/Twig/Function.php new file mode 100644 index 0000000..9fc76a8 --- /dev/null +++ b/public/system/library/template/Twig/Function.php @@ -0,0 +1,74 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Function class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleFunction instead.', E_USER_DEPRECATED); + +/** + * Represents a template function. + * + * Use Twig_SimpleFunction instead. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +abstract class Twig_Function implements Twig_FunctionInterface, Twig_FunctionCallableInterface +{ + protected $options; + protected $arguments = array(); + + public function __construct(array $options = array()) + { + $this->options = array_merge(array( + 'needs_environment' => false, + 'needs_context' => false, + 'callable' => null, + ), $options); + } + + public function setArguments($arguments) + { + $this->arguments = $arguments; + } + + public function getArguments() + { + return $this->arguments; + } + + public function needsEnvironment() + { + return $this->options['needs_environment']; + } + + public function needsContext() + { + return $this->options['needs_context']; + } + + public function getSafe(Twig_Node $functionArgs) + { + if (isset($this->options['is_safe'])) { + return $this->options['is_safe']; + } + + if (isset($this->options['is_safe_callback'])) { + return call_user_func($this->options['is_safe_callback'], $functionArgs); + } + + return array(); + } + + public function getCallable() + { + return $this->options['callable']; + } +} diff --git a/public/system/library/template/Twig/Function/Function.php b/public/system/library/template/Twig/Function/Function.php new file mode 100644 index 0000000..ae83e15 --- /dev/null +++ b/public/system/library/template/Twig/Function/Function.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2010 Arnaud Le Blanc + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Function_Function class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleFunction instead.', E_USER_DEPRECATED); + +/** + * Represents a function template function. + * + * Use Twig_SimpleFunction instead. + * + * @author Arnaud Le Blanc <arnaud.lb@gmail.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +class Twig_Function_Function extends Twig_Function +{ + protected $function; + + public function __construct($function, array $options = array()) + { + $options['callable'] = $function; + + parent::__construct($options); + + $this->function = $function; + } + + public function compile() + { + return $this->function; + } +} diff --git a/public/system/library/template/Twig/Function/Method.php b/public/system/library/template/Twig/Function/Method.php new file mode 100644 index 0000000..ba9945e --- /dev/null +++ b/public/system/library/template/Twig/Function/Method.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2010 Arnaud Le Blanc + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Function_Method class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleFunction instead.', E_USER_DEPRECATED); + +/** + * Represents a method template function. + * + * Use Twig_SimpleFunction instead. + * + * @author Arnaud Le Blanc <arnaud.lb@gmail.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +class Twig_Function_Method extends Twig_Function +{ + protected $extension; + protected $method; + + public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array()) + { + $options['callable'] = array($extension, $method); + + parent::__construct($options); + + $this->extension = $extension; + $this->method = $method; + } + + public function compile() + { + return sprintf('$this->env->getExtension(\'%s\')->%s', $this->extension->getName(), $this->method); + } +} diff --git a/public/system/library/template/Twig/Function/Node.php b/public/system/library/template/Twig/Function/Node.php new file mode 100644 index 0000000..118b0ba --- /dev/null +++ b/public/system/library/template/Twig/Function/Node.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Function_Node class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleFunction instead.', E_USER_DEPRECATED); + +/** + * Represents a template function as a node. + * + * Use Twig_SimpleFunction instead. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +class Twig_Function_Node extends Twig_Function +{ + protected $class; + + public function __construct($class, array $options = array()) + { + parent::__construct($options); + + $this->class = $class; + } + + public function getClass() + { + return $this->class; + } + + public function compile() + { + } +} diff --git a/public/system/library/template/Twig/FunctionCallableInterface.php b/public/system/library/template/Twig/FunctionCallableInterface.php new file mode 100644 index 0000000..87d795e --- /dev/null +++ b/public/system/library/template/Twig/FunctionCallableInterface.php @@ -0,0 +1,24 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a callable template function. + * + * Use Twig_SimpleFunction instead. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_FunctionCallableInterface +{ + public function getCallable(); +} diff --git a/public/system/library/template/Twig/FunctionInterface.php b/public/system/library/template/Twig/FunctionInterface.php new file mode 100644 index 0000000..f449234 --- /dev/null +++ b/public/system/library/template/Twig/FunctionInterface.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * (c) 2010 Arnaud Le Blanc + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a template function. + * + * Use Twig_SimpleFunction instead. + * + * @author Arnaud Le Blanc <arnaud.lb@gmail.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_FunctionInterface +{ + /** + * Compiles a function. + * + * @return string The PHP code for the function + */ + public function compile(); + + public function needsEnvironment(); + + public function needsContext(); + + public function getSafe(Twig_Node $filterArgs); + + public function setArguments($arguments); + + public function getArguments(); +} diff --git a/public/system/library/template/Twig/Lexer.php b/public/system/library/template/Twig/Lexer.php new file mode 100644 index 0000000..dd79873 --- /dev/null +++ b/public/system/library/template/Twig/Lexer.php @@ -0,0 +1,411 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Lexes a template string. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Lexer implements Twig_LexerInterface +{ + protected $tokens; + protected $code; + protected $cursor; + protected $lineno; + protected $end; + protected $state; + protected $states; + protected $brackets; + protected $env; + protected $filename; + protected $options; + protected $regexes; + protected $position; + protected $positions; + protected $currentVarBlockLine; + + const STATE_DATA = 0; + const STATE_BLOCK = 1; + const STATE_VAR = 2; + const STATE_STRING = 3; + const STATE_INTERPOLATION = 4; + + const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'; + const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?/A'; + const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; + const REGEX_DQ_STRING_DELIM = '/"/A'; + const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; + const PUNCTUATION = '()[]{}?:.,|'; + + public function __construct(Twig_Environment $env, array $options = array()) + { + $this->env = $env; + + $this->options = array_merge(array( + 'tag_comment' => array('{#', '#}'), + 'tag_block' => array('{%', '%}'), + 'tag_variable' => array('{{', '}}'), + 'whitespace_trim' => '-', + 'interpolation' => array('#{', '}'), + ), $options); + + $this->regexes = array( + 'lex_var' => '/\s*'.preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_variable'][1], '/').'/A', + 'lex_block' => '/\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')\n?/A', + 'lex_raw_data' => '/('.preg_quote($this->options['tag_block'][0].$this->options['whitespace_trim'], '/').'|'.preg_quote($this->options['tag_block'][0], '/').')\s*(?:end%s)\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/s', + 'operator' => $this->getOperatorRegex(), + 'lex_comment' => '/(?:'.preg_quote($this->options['whitespace_trim'], '/').preg_quote($this->options['tag_comment'][1], '/').'\s*|'.preg_quote($this->options['tag_comment'][1], '/').')\n?/s', + 'lex_block_raw' => '/\s*(raw|verbatim)\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/As', + 'lex_block_line' => '/\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '/').'/As', + 'lex_tokens_start' => '/('.preg_quote($this->options['tag_variable'][0], '/').'|'.preg_quote($this->options['tag_block'][0], '/').'|'.preg_quote($this->options['tag_comment'][0], '/').')('.preg_quote($this->options['whitespace_trim'], '/').')?/s', + 'interpolation_start' => '/'.preg_quote($this->options['interpolation'][0], '/').'\s*/A', + 'interpolation_end' => '/\s*'.preg_quote($this->options['interpolation'][1], '/').'/A', + ); + } + + /** + * {@inheritdoc} + */ + public function tokenize($code, $filename = null) + { + if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) { + $mbEncoding = mb_internal_encoding(); + mb_internal_encoding('ASCII'); + } else { + $mbEncoding = null; + } + + $this->code = str_replace(array("\r\n", "\r"), "\n", $code); + $this->filename = $filename; + $this->cursor = 0; + $this->lineno = 1; + $this->end = strlen($this->code); + $this->tokens = array(); + $this->state = self::STATE_DATA; + $this->states = array(); + $this->brackets = array(); + $this->position = -1; + + // find all token starts in one go + preg_match_all($this->regexes['lex_tokens_start'], $this->code, $matches, PREG_OFFSET_CAPTURE); + $this->positions = $matches; + + while ($this->cursor < $this->end) { + // dispatch to the lexing functions depending + // on the current state + switch ($this->state) { + case self::STATE_DATA: + $this->lexData(); + break; + + case self::STATE_BLOCK: + $this->lexBlock(); + break; + + case self::STATE_VAR: + $this->lexVar(); + break; + + case self::STATE_STRING: + $this->lexString(); + break; + + case self::STATE_INTERPOLATION: + $this->lexInterpolation(); + break; + } + } + + $this->pushToken(Twig_Token::EOF_TYPE); + + if (!empty($this->brackets)) { + list($expect, $lineno) = array_pop($this->brackets); + throw new Twig_Error_Syntax(sprintf('Unclosed "%s".', $expect), $lineno, $this->filename); + } + + if ($mbEncoding) { + mb_internal_encoding($mbEncoding); + } + + return new Twig_TokenStream($this->tokens, $this->filename); + } + + protected function lexData() + { + // if no matches are left we return the rest of the template as simple text token + if ($this->position == count($this->positions[0]) - 1) { + $this->pushToken(Twig_Token::TEXT_TYPE, substr($this->code, $this->cursor)); + $this->cursor = $this->end; + + return; + } + + // Find the first token after the current cursor + $position = $this->positions[0][++$this->position]; + while ($position[1] < $this->cursor) { + if ($this->position == count($this->positions[0]) - 1) { + return; + } + $position = $this->positions[0][++$this->position]; + } + + // push the template text first + $text = $textContent = substr($this->code, $this->cursor, $position[1] - $this->cursor); + if (isset($this->positions[2][$this->position][0])) { + $text = rtrim($text); + } + $this->pushToken(Twig_Token::TEXT_TYPE, $text); + $this->moveCursor($textContent.$position[0]); + + switch ($this->positions[1][$this->position][0]) { + case $this->options['tag_comment'][0]: + $this->lexComment(); + break; + + case $this->options['tag_block'][0]: + // raw data? + if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, null, $this->cursor)) { + $this->moveCursor($match[0]); + $this->lexRawData($match[1]); + // {% line \d+ %} + } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, null, $this->cursor)) { + $this->moveCursor($match[0]); + $this->lineno = (int) $match[1]; + } else { + $this->pushToken(Twig_Token::BLOCK_START_TYPE); + $this->pushState(self::STATE_BLOCK); + $this->currentVarBlockLine = $this->lineno; + } + break; + + case $this->options['tag_variable'][0]: + $this->pushToken(Twig_Token::VAR_START_TYPE); + $this->pushState(self::STATE_VAR); + $this->currentVarBlockLine = $this->lineno; + break; + } + } + + protected function lexBlock() + { + if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, null, $this->cursor)) { + $this->pushToken(Twig_Token::BLOCK_END_TYPE); + $this->moveCursor($match[0]); + $this->popState(); + } else { + $this->lexExpression(); + } + } + + protected function lexVar() + { + if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, null, $this->cursor)) { + $this->pushToken(Twig_Token::VAR_END_TYPE); + $this->moveCursor($match[0]); + $this->popState(); + } else { + $this->lexExpression(); + } + } + + protected function lexExpression() + { + // whitespace + if (preg_match('/\s+/A', $this->code, $match, null, $this->cursor)) { + $this->moveCursor($match[0]); + + if ($this->cursor >= $this->end) { + throw new Twig_Error_Syntax(sprintf('Unclosed "%s".', $this->state === self::STATE_BLOCK ? 'block' : 'variable'), $this->currentVarBlockLine, $this->filename); + } + } + + // operators + if (preg_match($this->regexes['operator'], $this->code, $match, null, $this->cursor)) { + $this->pushToken(Twig_Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0])); + $this->moveCursor($match[0]); + } + // names + elseif (preg_match(self::REGEX_NAME, $this->code, $match, null, $this->cursor)) { + $this->pushToken(Twig_Token::NAME_TYPE, $match[0]); + $this->moveCursor($match[0]); + } + // numbers + elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, null, $this->cursor)) { + $number = (float) $match[0]; // floats + if (ctype_digit($match[0]) && $number <= PHP_INT_MAX) { + $number = (int) $match[0]; // integers lower than the maximum + } + $this->pushToken(Twig_Token::NUMBER_TYPE, $number); + $this->moveCursor($match[0]); + } + // punctuation + elseif (false !== strpos(self::PUNCTUATION, $this->code[$this->cursor])) { + // opening bracket + if (false !== strpos('([{', $this->code[$this->cursor])) { + $this->brackets[] = array($this->code[$this->cursor], $this->lineno); + } + // closing bracket + elseif (false !== strpos(')]}', $this->code[$this->cursor])) { + if (empty($this->brackets)) { + throw new Twig_Error_Syntax(sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->filename); + } + + list($expect, $lineno) = array_pop($this->brackets); + if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) { + throw new Twig_Error_Syntax(sprintf('Unclosed "%s".', $expect), $lineno, $this->filename); + } + } + + $this->pushToken(Twig_Token::PUNCTUATION_TYPE, $this->code[$this->cursor]); + ++$this->cursor; + } + // strings + elseif (preg_match(self::REGEX_STRING, $this->code, $match, null, $this->cursor)) { + $this->pushToken(Twig_Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1))); + $this->moveCursor($match[0]); + } + // opening double quoted string + elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, null, $this->cursor)) { + $this->brackets[] = array('"', $this->lineno); + $this->pushState(self::STATE_STRING); + $this->moveCursor($match[0]); + } + // unlexable + else { + throw new Twig_Error_Syntax(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->filename); + } + } + + protected function lexRawData($tag) + { + if ('raw' === $tag) { + @trigger_error(sprintf('Twig Tag "raw" is deprecated since version 1.21. Use "verbatim" instead in %s at line %d.', $this->filename, $this->lineno), E_USER_DEPRECATED); + } + + if (!preg_match(str_replace('%s', $tag, $this->regexes['lex_raw_data']), $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { + throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "%s" block.', $tag), $this->lineno, $this->filename); + } + + $text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor); + $this->moveCursor($text.$match[0][0]); + + if (false !== strpos($match[1][0], $this->options['whitespace_trim'])) { + $text = rtrim($text); + } + + $this->pushToken(Twig_Token::TEXT_TYPE, $text); + } + + protected function lexComment() + { + if (!preg_match($this->regexes['lex_comment'], $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { + throw new Twig_Error_Syntax('Unclosed comment.', $this->lineno, $this->filename); + } + + $this->moveCursor(substr($this->code, $this->cursor, $match[0][1] - $this->cursor).$match[0][0]); + } + + protected function lexString() + { + if (preg_match($this->regexes['interpolation_start'], $this->code, $match, null, $this->cursor)) { + $this->brackets[] = array($this->options['interpolation'][0], $this->lineno); + $this->pushToken(Twig_Token::INTERPOLATION_START_TYPE); + $this->moveCursor($match[0]); + $this->pushState(self::STATE_INTERPOLATION); + } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, null, $this->cursor) && strlen($match[0]) > 0) { + $this->pushToken(Twig_Token::STRING_TYPE, stripcslashes($match[0])); + $this->moveCursor($match[0]); + } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, null, $this->cursor)) { + list($expect, $lineno) = array_pop($this->brackets); + if ($this->code[$this->cursor] != '"') { + throw new Twig_Error_Syntax(sprintf('Unclosed "%s".', $expect), $lineno, $this->filename); + } + + $this->popState(); + ++$this->cursor; + } + } + + protected function lexInterpolation() + { + $bracket = end($this->brackets); + if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, null, $this->cursor)) { + array_pop($this->brackets); + $this->pushToken(Twig_Token::INTERPOLATION_END_TYPE); + $this->moveCursor($match[0]); + $this->popState(); + } else { + $this->lexExpression(); + } + } + + protected function pushToken($type, $value = '') + { + // do not push empty text tokens + if (Twig_Token::TEXT_TYPE === $type && '' === $value) { + return; + } + + $this->tokens[] = new Twig_Token($type, $value, $this->lineno); + } + + protected function moveCursor($text) + { + $this->cursor += strlen($text); + $this->lineno += substr_count($text, "\n"); + } + + protected function getOperatorRegex() + { + $operators = array_merge( + array('='), + array_keys($this->env->getUnaryOperators()), + array_keys($this->env->getBinaryOperators()) + ); + + $operators = array_combine($operators, array_map('strlen', $operators)); + arsort($operators); + + $regex = array(); + foreach ($operators as $operator => $length) { + // an operator that ends with a character must be followed by + // a whitespace or a parenthesis + if (ctype_alpha($operator[$length - 1])) { + $r = preg_quote($operator, '/').'(?=[\s()])'; + } else { + $r = preg_quote($operator, '/'); + } + + // an operator with a space can be any amount of whitespaces + $r = preg_replace('/\s+/', '\s+', $r); + + $regex[] = $r; + } + + return '/'.implode('|', $regex).'/A'; + } + + protected function pushState($state) + { + $this->states[] = $this->state; + $this->state = $state; + } + + protected function popState() + { + if (0 === count($this->states)) { + throw new Exception('Cannot pop state without a previous state'); + } + + $this->state = array_pop($this->states); + } +} diff --git a/public/system/library/template/Twig/LexerInterface.php b/public/system/library/template/Twig/LexerInterface.php new file mode 100644 index 0000000..24a9478 --- /dev/null +++ b/public/system/library/template/Twig/LexerInterface.php @@ -0,0 +1,32 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface implemented by lexer classes. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 3.0) + */ +interface Twig_LexerInterface +{ + /** + * Tokenizes a source code. + * + * @param string $code The source code + * @param string $filename A unique identifier for the source code + * + * @return Twig_TokenStream A token stream instance + * + * @throws Twig_Error_Syntax When the code is syntactically wrong + */ + public function tokenize($code, $filename = null); +} diff --git a/public/system/library/template/Twig/Loader/Array.php b/public/system/library/template/Twig/Loader/Array.php new file mode 100644 index 0000000..90221d5 --- /dev/null +++ b/public/system/library/template/Twig/Loader/Array.php @@ -0,0 +1,95 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Loads a template from an array. + * + * When using this loader with a cache mechanism, you should know that a new cache + * key is generated each time a template content "changes" (the cache key being the + * source code of the template). If you don't want to see your cache grows out of + * control, you need to take care of clearing the old cache file by yourself. + * + * This loader should only be used for unit testing. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Loader_Array implements Twig_LoaderInterface, Twig_ExistsLoaderInterface +{ + protected $templates = array(); + + /** + * Constructor. + * + * @param array $templates An array of templates (keys are the names, and values are the source code) + */ + public function __construct(array $templates) + { + $this->templates = $templates; + } + + /** + * Adds or overrides a template. + * + * @param string $name The template name + * @param string $template The template source + */ + public function setTemplate($name, $template) + { + $this->templates[(string) $name] = $template; + } + + /** + * {@inheritdoc} + */ + public function getSource($name) + { + $name = (string) $name; + if (!isset($this->templates[$name])) { + throw new Twig_Error_Loader(sprintf('Template "%s" is not defined.', $name)); + } + + return $this->templates[$name]; + } + + /** + * {@inheritdoc} + */ + public function exists($name) + { + return isset($this->templates[(string) $name]); + } + + /** + * {@inheritdoc} + */ + public function getCacheKey($name) + { + $name = (string) $name; + if (!isset($this->templates[$name])) { + throw new Twig_Error_Loader(sprintf('Template "%s" is not defined.', $name)); + } + + return $this->templates[$name]; + } + + /** + * {@inheritdoc} + */ + public function isFresh($name, $time) + { + $name = (string) $name; + if (!isset($this->templates[$name])) { + throw new Twig_Error_Loader(sprintf('Template "%s" is not defined.', $name)); + } + + return true; + } +} diff --git a/public/system/library/template/Twig/Loader/Chain.php b/public/system/library/template/Twig/Loader/Chain.php new file mode 100644 index 0000000..81d57ad --- /dev/null +++ b/public/system/library/template/Twig/Loader/Chain.php @@ -0,0 +1,138 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Loads templates from other loaders. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Loader_Chain implements Twig_LoaderInterface, Twig_ExistsLoaderInterface +{ + private $hasSourceCache = array(); + protected $loaders = array(); + + /** + * Constructor. + * + * @param Twig_LoaderInterface[] $loaders An array of loader instances + */ + public function __construct(array $loaders = array()) + { + foreach ($loaders as $loader) { + $this->addLoader($loader); + } + } + + /** + * Adds a loader instance. + * + * @param Twig_LoaderInterface $loader A Loader instance + */ + public function addLoader(Twig_LoaderInterface $loader) + { + $this->loaders[] = $loader; + $this->hasSourceCache = array(); + } + + /** + * {@inheritdoc} + */ + public function getSource($name) + { + $exceptions = array(); + foreach ($this->loaders as $loader) { + if ($loader instanceof Twig_ExistsLoaderInterface && !$loader->exists($name)) { + continue; + } + + try { + return $loader->getSource($name); + } catch (Twig_Error_Loader $e) { + $exceptions[] = $e->getMessage(); + } + } + + throw new Twig_Error_Loader(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + } + + /** + * {@inheritdoc} + */ + public function exists($name) + { + $name = (string) $name; + + if (isset($this->hasSourceCache[$name])) { + return $this->hasSourceCache[$name]; + } + + foreach ($this->loaders as $loader) { + if ($loader instanceof Twig_ExistsLoaderInterface) { + if ($loader->exists($name)) { + return $this->hasSourceCache[$name] = true; + } + + continue; + } + + try { + $loader->getSource($name); + + return $this->hasSourceCache[$name] = true; + } catch (Twig_Error_Loader $e) { + } + } + + return $this->hasSourceCache[$name] = false; + } + + /** + * {@inheritdoc} + */ + public function getCacheKey($name) + { + $exceptions = array(); + foreach ($this->loaders as $loader) { + if ($loader instanceof Twig_ExistsLoaderInterface && !$loader->exists($name)) { + continue; + } + + try { + return $loader->getCacheKey($name); + } catch (Twig_Error_Loader $e) { + $exceptions[] = get_class($loader).': '.$e->getMessage(); + } + } + + throw new Twig_Error_Loader(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + } + + /** + * {@inheritdoc} + */ + public function isFresh($name, $time) + { + $exceptions = array(); + foreach ($this->loaders as $loader) { + if ($loader instanceof Twig_ExistsLoaderInterface && !$loader->exists($name)) { + continue; + } + + try { + return $loader->isFresh($name, $time); + } catch (Twig_Error_Loader $e) { + $exceptions[] = get_class($loader).': '.$e->getMessage(); + } + } + + throw new Twig_Error_Loader(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + } +} diff --git a/public/system/library/template/Twig/Loader/Filesystem.php b/public/system/library/template/Twig/Loader/Filesystem.php new file mode 100644 index 0000000..1bc75a1 --- /dev/null +++ b/public/system/library/template/Twig/Loader/Filesystem.php @@ -0,0 +1,260 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Loads template from the filesystem. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderInterface +{ + /** Identifier of the main namespace. */ + const MAIN_NAMESPACE = '__main__'; + + protected $paths = array(); + protected $cache = array(); + protected $errorCache = array(); + + /** + * Constructor. + * + * @param string|array $paths A path or an array of paths where to look for templates + */ + public function __construct($paths = array()) + { + if ($paths) { + $this->setPaths($paths); + } + } + + /** + * Returns the paths to the templates. + * + * @param string $namespace A path namespace + * + * @return array The array of paths where to look for templates + */ + public function getPaths($namespace = self::MAIN_NAMESPACE) + { + return isset($this->paths[$namespace]) ? $this->paths[$namespace] : array(); + } + + /** + * Returns the path namespaces. + * + * The main namespace is always defined. + * + * @return array The array of defined namespaces + */ + public function getNamespaces() + { + return array_keys($this->paths); + } + + /** + * Sets the paths where templates are stored. + * + * @param string|array $paths A path or an array of paths where to look for templates + * @param string $namespace A path namespace + */ + public function setPaths($paths, $namespace = self::MAIN_NAMESPACE) + { + if (!is_array($paths)) { + $paths = array($paths); + } + + $this->paths[$namespace] = array(); + foreach ($paths as $path) { + $this->addPath($path, $namespace); + } + } + + /** + * Adds a path where templates are stored. + * + * @param string $path A path where to look for templates + * @param string $namespace A path name + * + * @throws Twig_Error_Loader + */ + public function addPath($path, $namespace = self::MAIN_NAMESPACE) + { + // invalidate the cache + $this->cache = $this->errorCache = array(); + + if (!is_dir($path)) { + throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist.', $path)); + } + + $this->paths[$namespace][] = rtrim($path, '/\\'); + } + + /** + * Prepends a path where templates are stored. + * + * @param string $path A path where to look for templates + * @param string $namespace A path name + * + * @throws Twig_Error_Loader + */ + public function prependPath($path, $namespace = self::MAIN_NAMESPACE) + { + // invalidate the cache + $this->cache = $this->errorCache = array(); + + if (!is_dir($path)) { + throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist.', $path)); + } + + $path = rtrim($path, '/\\'); + + if (!isset($this->paths[$namespace])) { + $this->paths[$namespace][] = $path; + } else { + array_unshift($this->paths[$namespace], $path); + } + } + + /** + * {@inheritdoc} + */ + public function getSource($name) + { + return file_get_contents($this->findTemplate($name)); + } + + /** + * {@inheritdoc} + */ + public function getCacheKey($name) + { + return $this->findTemplate($name); + } + + /** + * {@inheritdoc} + */ + public function exists($name) + { + $name = $this->normalizeName($name); + + if (isset($this->cache[$name])) { + return true; + } + + try { + return false !== $this->findTemplate($name, false); + } catch (Twig_Error_Loader $exception) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function isFresh($name, $time) + { + return filemtime($this->findTemplate($name)) <= $time; + } + + protected function findTemplate($name) + { + $throw = func_num_args() > 1 ? func_get_arg(1) : true; + $name = $this->normalizeName($name); + + if (isset($this->cache[$name])) { + return $this->cache[$name]; + } + + if (isset($this->errorCache[$name])) { + if (!$throw) { + return false; + } + + throw new Twig_Error_Loader($this->errorCache[$name]); + } + + $this->validateName($name); + + list($namespace, $shortname) = $this->parseName($name); + + if (!isset($this->paths[$namespace])) { + $this->errorCache[$name] = sprintf('There are no registered paths for namespace "%s".', $namespace); + + if (!$throw) { + return false; + } + + throw new Twig_Error_Loader($this->errorCache[$name]); + } + + foreach ($this->paths[$namespace] as $path) { + if (is_file($path.'/'.$shortname)) { + if (false !== $realpath = realpath($path.'/'.$shortname)) { + return $this->cache[$name] = $realpath; + } + + return $this->cache[$name] = $path.'/'.$shortname; + } + } + + $this->errorCache[$name] = sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])); + + if (!$throw) { + return false; + } + + throw new Twig_Error_Loader($this->errorCache[$name]); + } + + protected function parseName($name, $default = self::MAIN_NAMESPACE) + { + if (isset($name[0]) && '@' == $name[0]) { + if (false === $pos = strpos($name, '/')) { + throw new Twig_Error_Loader(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name)); + } + + $namespace = substr($name, 1, $pos - 1); + $shortname = substr($name, $pos + 1); + + return array($namespace, $shortname); + } + + return array($default, $name); + } + + protected function normalizeName($name) + { + return preg_replace('#/{2,}#', '/', str_replace('\\', '/', (string) $name)); + } + + protected function validateName($name) + { + if (false !== strpos($name, "\0")) { + throw new Twig_Error_Loader('A template name cannot contain NUL bytes.'); + } + + $name = ltrim($name, '/'); + $parts = explode('/', $name); + $level = 0; + foreach ($parts as $part) { + if ('..' === $part) { + --$level; + } elseif ('.' !== $part) { + ++$level; + } + + if ($level < 0) { + throw new Twig_Error_Loader(sprintf('Looks like you try to load a template outside configured directories (%s).', $name)); + } + } + } +} diff --git a/public/system/library/template/Twig/Loader/String.php b/public/system/library/template/Twig/Loader/String.php new file mode 100644 index 0000000..00f507a --- /dev/null +++ b/public/system/library/template/Twig/Loader/String.php @@ -0,0 +1,63 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Loader_String class is deprecated since version 1.18.1 and will be removed in 2.0. Use Twig_Loader_Array instead or Twig_Environment::createTemplate().', E_USER_DEPRECATED); + +/** + * Loads a template from a string. + * + * This loader should NEVER be used. It only exists for Twig internal purposes. + * + * When using this loader with a cache mechanism, you should know that a new cache + * key is generated each time a template content "changes" (the cache key being the + * source code of the template). If you don't want to see your cache grows out of + * control, you need to take care of clearing the old cache file by yourself. + * + * @deprecated since 1.18.1 (to be removed in 2.0) + * + * @internal + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Loader_String implements Twig_LoaderInterface, Twig_ExistsLoaderInterface +{ + /** + * {@inheritdoc} + */ + public function getSource($name) + { + return $name; + } + + /** + * {@inheritdoc} + */ + public function exists($name) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getCacheKey($name) + { + return $name; + } + + /** + * {@inheritdoc} + */ + public function isFresh($name, $time) + { + return true; + } +} diff --git a/public/system/library/template/Twig/LoaderInterface.php b/public/system/library/template/Twig/LoaderInterface.php new file mode 100644 index 0000000..544ea4e --- /dev/null +++ b/public/system/library/template/Twig/LoaderInterface.php @@ -0,0 +1,53 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface all loaders must implement. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +interface Twig_LoaderInterface +{ + /** + * Gets the source code of a template, given its name. + * + * @param string $name The name of the template to load + * + * @return string The template source code + * + * @throws Twig_Error_Loader When $name is not found + */ + public function getSource($name); + + /** + * Gets the cache key to use for the cache for a given template name. + * + * @param string $name The name of the template to load + * + * @return string The cache key + * + * @throws Twig_Error_Loader When $name is not found + */ + public function getCacheKey($name); + + /** + * Returns true if the template is still fresh. + * + * @param string $name The template name + * @param int $time Timestamp of the last modification time of the + * cached template + * + * @return bool true if the template is fresh, false otherwise + * + * @throws Twig_Error_Loader When $name is not found + */ + public function isFresh($name, $time); +} diff --git a/public/system/library/template/Twig/Markup.php b/public/system/library/template/Twig/Markup.php new file mode 100644 index 0000000..69871fc --- /dev/null +++ b/public/system/library/template/Twig/Markup.php @@ -0,0 +1,37 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Marks a content as safe. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Markup implements Countable +{ + protected $content; + protected $charset; + + public function __construct($content, $charset) + { + $this->content = (string) $content; + $this->charset = $charset; + } + + public function __toString() + { + return $this->content; + } + + public function count() + { + return function_exists('mb_get_info') ? mb_strlen($this->content, $this->charset) : strlen($this->content); + } +} diff --git a/public/system/library/template/Twig/Node.php b/public/system/library/template/Twig/Node.php new file mode 100644 index 0000000..3065f67 --- /dev/null +++ b/public/system/library/template/Twig/Node.php @@ -0,0 +1,231 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a node in the AST. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node implements Twig_NodeInterface +{ + protected $nodes; + protected $attributes; + protected $lineno; + protected $tag; + + /** + * Constructor. + * + * The nodes are automatically made available as properties ($this->node). + * The attributes are automatically made available as array items ($this['name']). + * + * @param array $nodes An array of named nodes + * @param array $attributes An array of attributes (should not be nodes) + * @param int $lineno The line number + * @param string $tag The tag name associated with the Node + */ + public function __construct(array $nodes = array(), array $attributes = array(), $lineno = 0, $tag = null) + { + $this->nodes = $nodes; + $this->attributes = $attributes; + $this->lineno = $lineno; + $this->tag = $tag; + } + + public function __toString() + { + $attributes = array(); + foreach ($this->attributes as $name => $value) { + $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); + } + + $repr = array(get_class($this).'('.implode(', ', $attributes)); + + if (count($this->nodes)) { + foreach ($this->nodes as $name => $node) { + $len = strlen($name) + 4; + $noderepr = array(); + foreach (explode("\n", (string) $node) as $line) { + $noderepr[] = str_repeat(' ', $len).$line; + } + + $repr[] = sprintf(' %s: %s', $name, ltrim(implode("\n", $noderepr))); + } + + $repr[] = ')'; + } else { + $repr[0] .= ')'; + } + + return implode("\n", $repr); + } + + /** + * @deprecated since 1.16.1 (to be removed in 2.0) + */ + public function toXml($asDom = false) + { + @trigger_error(sprintf('%s is deprecated since version 1.16.1 and will be removed in 2.0.', __METHOD__), E_USER_DEPRECATED); + + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $dom->appendChild($xml = $dom->createElement('twig')); + + $xml->appendChild($node = $dom->createElement('node')); + $node->setAttribute('class', get_class($this)); + + foreach ($this->attributes as $name => $value) { + $node->appendChild($attribute = $dom->createElement('attribute')); + $attribute->setAttribute('name', $name); + $attribute->appendChild($dom->createTextNode($value)); + } + + foreach ($this->nodes as $name => $n) { + if (null === $n) { + continue; + } + + $child = $n->toXml(true)->getElementsByTagName('node')->item(0); + $child = $dom->importNode($child, true); + $child->setAttribute('name', $name); + + $node->appendChild($child); + } + + return $asDom ? $dom : $dom->saveXML(); + } + + public function compile(Twig_Compiler $compiler) + { + foreach ($this->nodes as $node) { + $node->compile($compiler); + } + } + + public function getLine() + { + return $this->lineno; + } + + public function getNodeTag() + { + return $this->tag; + } + + /** + * Returns true if the attribute is defined. + * + * @param string $name The attribute name + * + * @return bool true if the attribute is defined, false otherwise + */ + public function hasAttribute($name) + { + return array_key_exists($name, $this->attributes); + } + + /** + * Gets an attribute value by name. + * + * @param string $name + * + * @return mixed + */ + public function getAttribute($name) + { + if (!array_key_exists($name, $this->attributes)) { + throw new LogicException(sprintf('Attribute "%s" does not exist for Node "%s".', $name, get_class($this))); + } + + return $this->attributes[$name]; + } + + /** + * Sets an attribute by name to a value. + * + * @param string $name + * @param mixed $value + */ + public function setAttribute($name, $value) + { + $this->attributes[$name] = $value; + } + + /** + * Removes an attribute by name. + * + * @param string $name + */ + public function removeAttribute($name) + { + unset($this->attributes[$name]); + } + + /** + * Returns true if the node with the given name exists. + * + * @param string $name + * + * @return bool + */ + public function hasNode($name) + { + return array_key_exists($name, $this->nodes); + } + + /** + * Gets a node by name. + * + * @param string $name + * + * @return Twig_Node + */ + public function getNode($name) + { + if (!array_key_exists($name, $this->nodes)) { + throw new LogicException(sprintf('Node "%s" does not exist for Node "%s".', $name, get_class($this))); + } + + return $this->nodes[$name]; + } + + /** + * Sets a node. + * + * @param string $name + * @param Twig_Node $node + */ + public function setNode($name, $node = null) + { + $this->nodes[$name] = $node; + } + + /** + * Removes a node by name. + * + * @param string $name + */ + public function removeNode($name) + { + unset($this->nodes[$name]); + } + + public function count() + { + return count($this->nodes); + } + + public function getIterator() + { + return new ArrayIterator($this->nodes); + } +} diff --git a/public/system/library/template/Twig/Node/AutoEscape.php b/public/system/library/template/Twig/Node/AutoEscape.php new file mode 100644 index 0000000..47cc998 --- /dev/null +++ b/public/system/library/template/Twig/Node/AutoEscape.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents an autoescape node. + * + * The value is the escaping strategy (can be html, js, ...) + * + * The true value is equivalent to html. + * + * If autoescaping is disabled, then the value is false. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_AutoEscape extends Twig_Node +{ + public function __construct($value, Twig_NodeInterface $body, $lineno, $tag = 'autoescape') + { + parent::__construct(array('body' => $body), array('value' => $value), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->subcompile($this->getNode('body')); + } +} diff --git a/public/system/library/template/Twig/Node/Block.php b/public/system/library/template/Twig/Node/Block.php new file mode 100644 index 0000000..a05af6f --- /dev/null +++ b/public/system/library/template/Twig/Node/Block.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a block node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Block extends Twig_Node +{ + public function __construct($name, Twig_NodeInterface $body, $lineno, $tag = null) + { + parent::__construct(array('body' => $body), array('name' => $name), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write(sprintf("public function block_%s(\$context, array \$blocks = array())\n", $this->getAttribute('name')), "{\n") + ->indent() + ; + + $compiler + ->subcompile($this->getNode('body')) + ->outdent() + ->write("}\n\n") + ; + } +} diff --git a/public/system/library/template/Twig/Node/BlockReference.php b/public/system/library/template/Twig/Node/BlockReference.php new file mode 100644 index 0000000..9cd1551 --- /dev/null +++ b/public/system/library/template/Twig/Node/BlockReference.php @@ -0,0 +1,32 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a block call node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_BlockReference extends Twig_Node implements Twig_NodeOutputInterface +{ + public function __construct($name, $lineno, $tag = null) + { + parent::__construct(array(), array('name' => $name), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) + ; + } +} diff --git a/public/system/library/template/Twig/Node/Body.php b/public/system/library/template/Twig/Node/Body.php new file mode 100644 index 0000000..3ffb134 --- /dev/null +++ b/public/system/library/template/Twig/Node/Body.php @@ -0,0 +1,19 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a body node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Body extends Twig_Node +{ +} diff --git a/public/system/library/template/Twig/Node/CheckSecurity.php b/public/system/library/template/Twig/Node/CheckSecurity.php new file mode 100644 index 0000000..b4a436a --- /dev/null +++ b/public/system/library/template/Twig/Node/CheckSecurity.php @@ -0,0 +1,78 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_CheckSecurity extends Twig_Node +{ + protected $usedFilters; + protected $usedTags; + protected $usedFunctions; + + public function __construct(array $usedFilters, array $usedTags, array $usedFunctions) + { + $this->usedFilters = $usedFilters; + $this->usedTags = $usedTags; + $this->usedFunctions = $usedFunctions; + + parent::__construct(); + } + + public function compile(Twig_Compiler $compiler) + { + $tags = $filters = $functions = array(); + foreach (array('tags', 'filters', 'functions') as $type) { + foreach ($this->{'used'.ucfirst($type)} as $name => $node) { + if ($node instanceof Twig_Node) { + ${$type}[$name] = $node->getLine(); + } else { + ${$type}[$node] = null; + } + } + } + + $compiler + ->write('$tags = ')->repr(array_filter($tags))->raw(";\n") + ->write('$filters = ')->repr(array_filter($filters))->raw(";\n") + ->write('$functions = ')->repr(array_filter($functions))->raw(";\n\n") + ->write("try {\n") + ->indent() + ->write("\$this->env->getExtension('sandbox')->checkSecurity(\n") + ->indent() + ->write(!$tags ? "array(),\n" : "array('".implode("', '", array_keys($tags))."'),\n") + ->write(!$filters ? "array(),\n" : "array('".implode("', '", array_keys($filters))."'),\n") + ->write(!$functions ? "array()\n" : "array('".implode("', '", array_keys($functions))."')\n") + ->outdent() + ->write(");\n") + ->outdent() + ->write("} catch (Twig_Sandbox_SecurityError \$e) {\n") + ->indent() + ->write("\$e->setTemplateFile(\$this->getTemplateName());\n\n") + ->write("if (\$e instanceof Twig_Sandbox_SecurityNotAllowedTagError && isset(\$tags[\$e->getTagName()])) {\n") + ->indent() + ->write("\$e->setTemplateLine(\$tags[\$e->getTagName()]);\n") + ->outdent() + ->write("} elseif (\$e instanceof Twig_Sandbox_SecurityNotAllowedFilterError && isset(\$filters[\$e->getFilterName()])) {\n") + ->indent() + ->write("\$e->setTemplateLine(\$filters[\$e->getFilterName()]);\n") + ->outdent() + ->write("} elseif (\$e instanceof Twig_Sandbox_SecurityNotAllowedFunctionError && isset(\$functions[\$e->getFunctionName()])) {\n") + ->indent() + ->write("\$e->setTemplateLine(\$functions[\$e->getFunctionName()]);\n") + ->outdent() + ->write("}\n\n") + ->write("throw \$e;\n") + ->outdent() + ->write("}\n\n") + ; + } +} diff --git a/public/system/library/template/Twig/Node/Do.php b/public/system/library/template/Twig/Node/Do.php new file mode 100644 index 0000000..14fb84e --- /dev/null +++ b/public/system/library/template/Twig/Node/Do.php @@ -0,0 +1,33 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a do node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Do extends Twig_Node +{ + public function __construct(Twig_Node_Expression $expr, $lineno, $tag = null) + { + parent::__construct(array('expr' => $expr), array(), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('') + ->subcompile($this->getNode('expr')) + ->raw(";\n") + ; + } +} diff --git a/public/system/library/template/Twig/Node/Embed.php b/public/system/library/template/Twig/Node/Embed.php new file mode 100644 index 0000000..a213040 --- /dev/null +++ b/public/system/library/template/Twig/Node/Embed.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents an embed node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Embed extends Twig_Node_Include +{ + // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) + public function __construct($filename, $index, Twig_Node_Expression $variables = null, $only = false, $ignoreMissing = false, $lineno, $tag = null) + { + parent::__construct(new Twig_Node_Expression_Constant('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag); + + $this->setAttribute('filename', $filename); + $this->setAttribute('index', $index); + } + + protected function addGetTemplate(Twig_Compiler $compiler) + { + $compiler + ->write('$this->loadTemplate(') + ->string($this->getAttribute('filename')) + ->raw(', ') + ->repr($compiler->getFilename()) + ->raw(', ') + ->repr($this->getLine()) + ->raw(', ') + ->string($this->getAttribute('index')) + ->raw(')') + ; + } +} diff --git a/public/system/library/template/Twig/Node/Expression.php b/public/system/library/template/Twig/Node/Expression.php new file mode 100644 index 0000000..a7382e7 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression.php @@ -0,0 +1,20 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Abstract class for all nodes that represents an expression. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +abstract class Twig_Node_Expression extends Twig_Node +{ +} diff --git a/public/system/library/template/Twig/Node/Expression/Array.php b/public/system/library/template/Twig/Node/Expression/Array.php new file mode 100644 index 0000000..83e583b --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Array.php @@ -0,0 +1,81 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Array extends Twig_Node_Expression +{ + protected $index; + + public function __construct(array $elements, $lineno) + { + parent::__construct($elements, array(), $lineno); + + $this->index = -1; + foreach ($this->getKeyValuePairs() as $pair) { + if ($pair['key'] instanceof Twig_Node_Expression_Constant && ctype_digit((string) $pair['key']->getAttribute('value')) && $pair['key']->getAttribute('value') > $this->index) { + $this->index = $pair['key']->getAttribute('value'); + } + } + } + + public function getKeyValuePairs() + { + $pairs = array(); + + foreach (array_chunk($this->nodes, 2) as $pair) { + $pairs[] = array( + 'key' => $pair[0], + 'value' => $pair[1], + ); + } + + return $pairs; + } + + public function hasElement(Twig_Node_Expression $key) + { + foreach ($this->getKeyValuePairs() as $pair) { + // we compare the string representation of the keys + // to avoid comparing the line numbers which are not relevant here. + if ((string) $key == (string) $pair['key']) { + return true; + } + } + + return false; + } + + public function addElement(Twig_Node_Expression $value, Twig_Node_Expression $key = null) + { + if (null === $key) { + $key = new Twig_Node_Expression_Constant(++$this->index, $value->getLine()); + } + + array_push($this->nodes, $key, $value); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->raw('array('); + $first = true; + foreach ($this->getKeyValuePairs() as $pair) { + if (!$first) { + $compiler->raw(', '); + } + $first = false; + + $compiler + ->subcompile($pair['key']) + ->raw(' => ') + ->subcompile($pair['value']) + ; + } + $compiler->raw(')'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/AssignName.php b/public/system/library/template/Twig/Node/Expression/AssignName.php new file mode 100644 index 0000000..ce0c5fb --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/AssignName.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +class Twig_Node_Expression_AssignName extends Twig_Node_Expression_Name +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('$context[') + ->string($this->getAttribute('name')) + ->raw(']') + ; + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary.php b/public/system/library/template/Twig/Node/Expression/Binary.php new file mode 100644 index 0000000..c821db5 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary.php @@ -0,0 +1,35 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +abstract class Twig_Node_Expression_Binary extends Twig_Node_Expression +{ + public function __construct(Twig_NodeInterface $left, Twig_NodeInterface $right, $lineno) + { + parent::__construct(array('left' => $left, 'right' => $right), array(), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('(') + ->subcompile($this->getNode('left')) + ->raw(' ') + ; + $this->operator($compiler); + $compiler + ->raw(' ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + abstract public function operator(Twig_Compiler $compiler); +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Add.php b/public/system/library/template/Twig/Node/Expression/Binary/Add.php new file mode 100644 index 0000000..0ef8e11 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Add.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Add extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('+'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/And.php b/public/system/library/template/Twig/Node/Expression/Binary/And.php new file mode 100644 index 0000000..d5752eb --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/And.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_And extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('&&'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/BitwiseAnd.php b/public/system/library/template/Twig/Node/Expression/Binary/BitwiseAnd.php new file mode 100644 index 0000000..9a46d84 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/BitwiseAnd.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_BitwiseAnd extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('&'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/BitwiseOr.php b/public/system/library/template/Twig/Node/Expression/Binary/BitwiseOr.php new file mode 100644 index 0000000..058a20b --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/BitwiseOr.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_BitwiseOr extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('|'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/BitwiseXor.php b/public/system/library/template/Twig/Node/Expression/Binary/BitwiseXor.php new file mode 100644 index 0000000..f4da73d --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/BitwiseXor.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_BitwiseXor extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('^'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Concat.php b/public/system/library/template/Twig/Node/Expression/Binary/Concat.php new file mode 100644 index 0000000..f9a6462 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Concat.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Concat extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('.'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Div.php b/public/system/library/template/Twig/Node/Expression/Binary/Div.php new file mode 100644 index 0000000..e0797a6 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Div.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Div extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('/'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/EndsWith.php b/public/system/library/template/Twig/Node/Expression/Binary/EndsWith.php new file mode 100644 index 0000000..93b3b96 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/EndsWith.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2013 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_EndsWith extends Twig_Node_Expression_Binary +{ + public function compile(Twig_Compiler $compiler) + { + $left = $compiler->getVarName(); + $right = $compiler->getVarName(); + $compiler + ->raw(sprintf('(is_string($%s = ', $left)) + ->subcompile($this->getNode('left')) + ->raw(sprintf(') && is_string($%s = ', $right)) + ->subcompile($this->getNode('right')) + ->raw(sprintf(') && (\'\' === $%2$s || $%2$s === substr($%1$s, -strlen($%2$s))))', $left, $right)) + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw(''); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Equal.php b/public/system/library/template/Twig/Node/Expression/Binary/Equal.php new file mode 100644 index 0000000..7b1236d --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Equal.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Equal extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('=='); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/FloorDiv.php b/public/system/library/template/Twig/Node/Expression/Binary/FloorDiv.php new file mode 100644 index 0000000..b606f6d --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/FloorDiv.php @@ -0,0 +1,24 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_FloorDiv extends Twig_Node_Expression_Binary +{ + public function compile(Twig_Compiler $compiler) + { + $compiler->raw('intval(floor('); + parent::compile($compiler); + $compiler->raw('))'); + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('/'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Greater.php b/public/system/library/template/Twig/Node/Expression/Binary/Greater.php new file mode 100644 index 0000000..a110bd9 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Greater.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Greater extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('>'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/GreaterEqual.php b/public/system/library/template/Twig/Node/Expression/Binary/GreaterEqual.php new file mode 100644 index 0000000..3754fed --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/GreaterEqual.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_GreaterEqual extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('>='); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/In.php b/public/system/library/template/Twig/Node/Expression/Binary/In.php new file mode 100644 index 0000000..9565a60 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/In.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_In extends Twig_Node_Expression_Binary +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('twig_in_filter(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('in'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Less.php b/public/system/library/template/Twig/Node/Expression/Binary/Less.php new file mode 100644 index 0000000..45fd300 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Less.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Less extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('<'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/LessEqual.php b/public/system/library/template/Twig/Node/Expression/Binary/LessEqual.php new file mode 100644 index 0000000..e38e257 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/LessEqual.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_LessEqual extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('<='); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Matches.php b/public/system/library/template/Twig/Node/Expression/Binary/Matches.php new file mode 100644 index 0000000..93bb292 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Matches.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2013 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Matches extends Twig_Node_Expression_Binary +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('preg_match(') + ->subcompile($this->getNode('right')) + ->raw(', ') + ->subcompile($this->getNode('left')) + ->raw(')') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw(''); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Mod.php b/public/system/library/template/Twig/Node/Expression/Binary/Mod.php new file mode 100644 index 0000000..9924114 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Mod.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Mod extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('%'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Mul.php b/public/system/library/template/Twig/Node/Expression/Binary/Mul.php new file mode 100644 index 0000000..c91529c --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Mul.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Mul extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('*'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/NotEqual.php b/public/system/library/template/Twig/Node/Expression/Binary/NotEqual.php new file mode 100644 index 0000000..26867ba --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/NotEqual.php @@ -0,0 +1,17 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_NotEqual extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('!='); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/NotIn.php b/public/system/library/template/Twig/Node/Expression/Binary/NotIn.php new file mode 100644 index 0000000..49ab39e --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/NotIn.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_NotIn extends Twig_Node_Expression_Binary +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('!twig_in_filter(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('not in'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Or.php b/public/system/library/template/Twig/Node/Expression/Binary/Or.php new file mode 100644 index 0000000..adba49c --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Or.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Or extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('||'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Power.php b/public/system/library/template/Twig/Node/Expression/Binary/Power.php new file mode 100644 index 0000000..cd6d046 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Power.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Power extends Twig_Node_Expression_Binary +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('pow(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('**'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Range.php b/public/system/library/template/Twig/Node/Expression/Binary/Range.php new file mode 100644 index 0000000..692ec9c --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Range.php @@ -0,0 +1,28 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Range extends Twig_Node_Expression_Binary +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('range(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('..'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/StartsWith.php b/public/system/library/template/Twig/Node/Expression/Binary/StartsWith.php new file mode 100644 index 0000000..d2e30d6 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/StartsWith.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2013 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_StartsWith extends Twig_Node_Expression_Binary +{ + public function compile(Twig_Compiler $compiler) + { + $left = $compiler->getVarName(); + $right = $compiler->getVarName(); + $compiler + ->raw(sprintf('(is_string($%s = ', $left)) + ->subcompile($this->getNode('left')) + ->raw(sprintf(') && is_string($%s = ', $right)) + ->subcompile($this->getNode('right')) + ->raw(sprintf(') && (\'\' === $%2$s || 0 === strpos($%1$s, $%2$s)))', $left, $right)) + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw(''); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Binary/Sub.php b/public/system/library/template/Twig/Node/Expression/Binary/Sub.php new file mode 100644 index 0000000..d446399 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Binary/Sub.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Binary_Sub extends Twig_Node_Expression_Binary +{ + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('-'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/BlockReference.php b/public/system/library/template/Twig/Node/Expression/BlockReference.php new file mode 100644 index 0000000..f6ed6ff --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/BlockReference.php @@ -0,0 +1,46 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a block call node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Expression_BlockReference extends Twig_Node_Expression +{ + public function __construct(Twig_NodeInterface $name, $asString = false, $lineno, $tag = null) + { + parent::__construct(array('name' => $name), array('as_string' => $asString, 'output' => false), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + if ($this->getAttribute('as_string')) { + $compiler->raw('(string) '); + } + + if ($this->getAttribute('output')) { + $compiler + ->addDebugInfo($this) + ->write('$this->displayBlock(') + ->subcompile($this->getNode('name')) + ->raw(", \$context, \$blocks);\n") + ; + } else { + $compiler + ->raw('$this->renderBlock(') + ->subcompile($this->getNode('name')) + ->raw(', $context, $blocks)') + ; + } + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Call.php b/public/system/library/template/Twig/Node/Expression/Call.php new file mode 100644 index 0000000..240553f --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Call.php @@ -0,0 +1,253 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +abstract class Twig_Node_Expression_Call extends Twig_Node_Expression +{ + protected function compileCallable(Twig_Compiler $compiler) + { + $closingParenthesis = false; + if ($this->hasAttribute('callable') && $callable = $this->getAttribute('callable')) { + if (is_string($callable)) { + $compiler->raw($callable); + } elseif (is_array($callable) && $callable[0] instanceof Twig_ExtensionInterface) { + $compiler->raw(sprintf('$this->env->getExtension(\'%s\')->%s', $callable[0]->getName(), $callable[1])); + } else { + $type = ucfirst($this->getAttribute('type')); + $compiler->raw(sprintf('call_user_func_array($this->env->get%s(\'%s\')->getCallable(), array', $type, $this->getAttribute('name'))); + $closingParenthesis = true; + } + } else { + $compiler->raw($this->getAttribute('thing')->compile()); + } + + $this->compileArguments($compiler); + + if ($closingParenthesis) { + $compiler->raw(')'); + } + } + + protected function compileArguments(Twig_Compiler $compiler) + { + $compiler->raw('('); + + $first = true; + + if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + $compiler->raw('$this->env'); + $first = false; + } + + if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->raw('$context'); + $first = false; + } + + if ($this->hasAttribute('arguments')) { + foreach ($this->getAttribute('arguments') as $argument) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->string($argument); + $first = false; + } + } + + if ($this->hasNode('node')) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->subcompile($this->getNode('node')); + $first = false; + } + + if ($this->hasNode('arguments') && null !== $this->getNode('arguments')) { + $callable = $this->hasAttribute('callable') ? $this->getAttribute('callable') : null; + + $arguments = $this->getArguments($callable, $this->getNode('arguments')); + + foreach ($arguments as $node) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->subcompile($node); + $first = false; + } + } + + $compiler->raw(')'); + } + + protected function getArguments($callable, $arguments) + { + $callType = $this->getAttribute('type'); + $callName = $this->getAttribute('name'); + + $parameters = array(); + $named = false; + foreach ($arguments as $name => $node) { + if (!is_int($name)) { + $named = true; + $name = $this->normalizeName($name); + } elseif ($named) { + throw new Twig_Error_Syntax(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName)); + } + + $parameters[$name] = $node; + } + + $isVariadic = $this->hasAttribute('is_variadic') && $this->getAttribute('is_variadic'); + if (!$named && !$isVariadic) { + return $parameters; + } + + if (!$callable) { + if ($named) { + $message = sprintf('Named arguments are not supported for %s "%s".', $callType, $callName); + } else { + $message = sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName); + } + + throw new LogicException($message); + } + + // manage named arguments + $callableParameters = $this->getCallableParameters($callable, $isVariadic); + $arguments = array(); + $names = array(); + $missingArguments = array(); + $optionalArguments = array(); + $pos = 0; + foreach ($callableParameters as $callableParameter) { + $names[] = $name = $this->normalizeName($callableParameter->name); + + if (array_key_exists($name, $parameters)) { + if (array_key_exists($pos, $parameters)) { + throw new Twig_Error_Syntax(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName)); + } + + if (!empty($missingArguments)) { + throw new Twig_Error_Syntax(sprintf( + 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', + $name, $callType, $callName, implode(', ', $names), count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)) + ); + } + + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $parameters[$name]; + unset($parameters[$name]); + $optionalArguments = array(); + } elseif (array_key_exists($pos, $parameters)) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $parameters[$pos]; + unset($parameters[$pos]); + $optionalArguments = array(); + ++$pos; + } elseif ($callableParameter->isDefaultValueAvailable()) { + $optionalArguments[] = new Twig_Node_Expression_Constant($callableParameter->getDefaultValue(), -1); + } elseif ($callableParameter->isOptional()) { + if (empty($parameters)) { + break; + } else { + $missingArguments[] = $name; + } + } else { + throw new Twig_Error_Syntax(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName)); + } + } + + if ($isVariadic) { + $arbitraryArguments = new Twig_Node_Expression_Array(array(), -1); + foreach ($parameters as $key => $value) { + if (is_int($key)) { + $arbitraryArguments->addElement($value); + } else { + $arbitraryArguments->addElement($value, new Twig_Node_Expression_Constant($key, -1)); + } + unset($parameters[$key]); + } + + if ($arbitraryArguments->count()) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $arbitraryArguments; + } + } + + if (!empty($parameters)) { + $unknownParameter = null; + foreach ($parameters as $parameter) { + if ($parameter instanceof Twig_Node) { + $unknownParameter = $parameter; + break; + } + } + + throw new Twig_Error_Syntax(sprintf( + 'Unknown argument%s "%s" for %s "%s(%s)".', + count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names) + ), $unknownParameter ? $unknownParameter->getLine() : -1); + } + + return $arguments; + } + + protected function normalizeName($name) + { + return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), $name)); + } + + private function getCallableParameters($callable, $isVariadic) + { + if (is_array($callable)) { + $r = new ReflectionMethod($callable[0], $callable[1]); + } elseif (is_object($callable) && !$callable instanceof Closure) { + $r = new ReflectionObject($callable); + $r = $r->getMethod('__invoke'); + } elseif (is_string($callable) && false !== strpos($callable, '::')) { + $r = new ReflectionMethod($callable); + } else { + $r = new ReflectionFunction($callable); + } + + $parameters = $r->getParameters(); + if ($this->hasNode('node')) { + array_shift($parameters); + } + if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + array_shift($parameters); + } + if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + array_shift($parameters); + } + if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) { + foreach ($this->getAttribute('arguments') as $argument) { + array_shift($parameters); + } + } + if ($isVariadic) { + $argument = end($parameters); + if ($argument && $argument->isArray() && $argument->isDefaultValueAvailable() && array() === $argument->getDefaultValue()) { + array_pop($parameters); + } else { + $callableName = $r->name; + if ($r->getDeclaringClass()) { + $callableName = $r->getDeclaringClass()->name.'::'.$callableName; + } + + throw new LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = array()".', $callableName, $this->getAttribute('type'), $this->getAttribute('name'))); + } + } + + return $parameters; + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Conditional.php b/public/system/library/template/Twig/Node/Expression/Conditional.php new file mode 100644 index 0000000..edcb1e2 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Conditional.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Conditional extends Twig_Node_Expression +{ + public function __construct(Twig_Node_Expression $expr1, Twig_Node_Expression $expr2, Twig_Node_Expression $expr3, $lineno) + { + parent::__construct(array('expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3), array(), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('((') + ->subcompile($this->getNode('expr1')) + ->raw(') ? (') + ->subcompile($this->getNode('expr2')) + ->raw(') : (') + ->subcompile($this->getNode('expr3')) + ->raw('))') + ; + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Constant.php b/public/system/library/template/Twig/Node/Expression/Constant.php new file mode 100644 index 0000000..a91dc69 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Constant.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Constant extends Twig_Node_Expression +{ + public function __construct($value, $lineno) + { + parent::__construct(array(), array('value' => $value), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->repr($this->getAttribute('value')); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/ExtensionReference.php b/public/system/library/template/Twig/Node/Expression/ExtensionReference.php new file mode 100644 index 0000000..b4882e3 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/ExtensionReference.php @@ -0,0 +1,32 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Node_Expression_ExtensionReference class is deprecated since version 1.23 and will be removed in 2.0.', E_USER_DEPRECATED); + +/** + * Represents an extension call node. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.23 and will be removed in 2.0. + */ +class Twig_Node_Expression_ExtensionReference extends Twig_Node_Expression +{ + public function __construct($name, $lineno, $tag = null) + { + parent::__construct(array(), array('name' => $name), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->raw(sprintf("\$this->env->getExtension('%s')", $this->getAttribute('name'))); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Filter.php b/public/system/library/template/Twig/Node/Expression/Filter.php new file mode 100644 index 0000000..a906232 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Filter.php @@ -0,0 +1,39 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Filter extends Twig_Node_Expression_Call +{ + public function __construct(Twig_NodeInterface $node, Twig_Node_Expression_Constant $filterName, Twig_NodeInterface $arguments, $lineno, $tag = null) + { + parent::__construct(array('node' => $node, 'filter' => $filterName, 'arguments' => $arguments), array(), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $name = $this->getNode('filter')->getAttribute('value'); + $filter = $compiler->getEnvironment()->getFilter($name); + + $this->setAttribute('name', $name); + $this->setAttribute('type', 'filter'); + $this->setAttribute('thing', $filter); + $this->setAttribute('needs_environment', $filter->needsEnvironment()); + $this->setAttribute('needs_context', $filter->needsContext()); + $this->setAttribute('arguments', $filter->getArguments()); + if ($filter instanceof Twig_FilterCallableInterface || $filter instanceof Twig_SimpleFilter) { + $this->setAttribute('callable', $filter->getCallable()); + } + if ($filter instanceof Twig_SimpleFilter) { + $this->setAttribute('is_variadic', $filter->isVariadic()); + } + + $this->compileCallable($compiler); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Filter/Default.php b/public/system/library/template/Twig/Node/Expression/Filter/Default.php new file mode 100644 index 0000000..1827c88 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Filter/Default.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Returns the value or the default value when it is undefined or empty. + * + * <pre> + * {{ var.foo|default('foo item on var is not defined') }} + * </pre> + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Expression_Filter_Default extends Twig_Node_Expression_Filter +{ + public function __construct(Twig_NodeInterface $node, Twig_Node_Expression_Constant $filterName, Twig_NodeInterface $arguments, $lineno, $tag = null) + { + $default = new Twig_Node_Expression_Filter($node, new Twig_Node_Expression_Constant('default', $node->getLine()), $arguments, $node->getLine()); + + if ('default' === $filterName->getAttribute('value') && ($node instanceof Twig_Node_Expression_Name || $node instanceof Twig_Node_Expression_GetAttr)) { + $test = new Twig_Node_Expression_Test_Defined(clone $node, 'defined', new Twig_Node(), $node->getLine()); + $false = count($arguments) ? $arguments->getNode(0) : new Twig_Node_Expression_Constant('', $node->getLine()); + + $node = new Twig_Node_Expression_Conditional($test, $default, $false, $node->getLine()); + } else { + $node = $default; + } + + parent::__construct($node, $filterName, $arguments, $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->subcompile($this->getNode('node')); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Function.php b/public/system/library/template/Twig/Node/Expression/Function.php new file mode 100644 index 0000000..7326ede --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Function.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Function extends Twig_Node_Expression_Call +{ + public function __construct($name, Twig_NodeInterface $arguments, $lineno) + { + parent::__construct(array('arguments' => $arguments), array('name' => $name), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $name = $this->getAttribute('name'); + $function = $compiler->getEnvironment()->getFunction($name); + + $this->setAttribute('name', $name); + $this->setAttribute('type', 'function'); + $this->setAttribute('thing', $function); + $this->setAttribute('needs_environment', $function->needsEnvironment()); + $this->setAttribute('needs_context', $function->needsContext()); + $this->setAttribute('arguments', $function->getArguments()); + if ($function instanceof Twig_FunctionCallableInterface || $function instanceof Twig_SimpleFunction) { + $this->setAttribute('callable', $function->getCallable()); + } + if ($function instanceof Twig_SimpleFunction) { + $this->setAttribute('is_variadic', $function->isVariadic()); + } + + $this->compileCallable($compiler); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/GetAttr.php b/public/system/library/template/Twig/Node/Expression/GetAttr.php new file mode 100644 index 0000000..6ce6111 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/GetAttr.php @@ -0,0 +1,63 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_GetAttr extends Twig_Node_Expression +{ + public function __construct(Twig_Node_Expression $node, Twig_Node_Expression $attribute, Twig_Node_Expression $arguments = null, $type, $lineno) + { + parent::__construct(array('node' => $node, 'attribute' => $attribute, 'arguments' => $arguments), array('type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'disable_c_ext' => false), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + if (function_exists('twig_template_get_attributes') && !$this->getAttribute('disable_c_ext')) { + $compiler->raw('twig_template_get_attributes($this, '); + } else { + $compiler->raw('$this->getAttribute('); + } + + if ($this->getAttribute('ignore_strict_check')) { + $this->getNode('node')->setAttribute('ignore_strict_check', true); + } + + $compiler->subcompile($this->getNode('node')); + + $compiler->raw(', ')->subcompile($this->getNode('attribute')); + + // only generate optional arguments when needed (to make generated code more readable) + $needFourth = $this->getAttribute('ignore_strict_check'); + $needThird = $needFourth || $this->getAttribute('is_defined_test'); + $needSecond = $needThird || Twig_Template::ANY_CALL !== $this->getAttribute('type'); + $needFirst = $needSecond || null !== $this->getNode('arguments'); + + if ($needFirst) { + if (null !== $this->getNode('arguments')) { + $compiler->raw(', ')->subcompile($this->getNode('arguments')); + } else { + $compiler->raw(', array()'); + } + } + + if ($needSecond) { + $compiler->raw(', ')->repr($this->getAttribute('type')); + } + + if ($needThird) { + $compiler->raw(', ')->repr($this->getAttribute('is_defined_test')); + } + + if ($needFourth) { + $compiler->raw(', ')->repr($this->getAttribute('ignore_strict_check')); + } + + $compiler->raw(')'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/MethodCall.php b/public/system/library/template/Twig/Node/Expression/MethodCall.php new file mode 100644 index 0000000..620b02b --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/MethodCall.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_MethodCall extends Twig_Node_Expression +{ + public function __construct(Twig_Node_Expression $node, $method, Twig_Node_Expression_Array $arguments, $lineno) + { + parent::__construct(array('node' => $node, 'arguments' => $arguments), array('method' => $method, 'safe' => false), $lineno); + + if ($node instanceof Twig_Node_Expression_Name) { + $node->setAttribute('always_defined', true); + } + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->subcompile($this->getNode('node')) + ->raw('->') + ->raw($this->getAttribute('method')) + ->raw('(') + ; + $first = true; + foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { + if (!$first) { + $compiler->raw(', '); + } + $first = false; + + $compiler->subcompile($pair['value']); + } + $compiler->raw(')'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Name.php b/public/system/library/template/Twig/Node/Expression/Name.php new file mode 100644 index 0000000..a6e0ff4 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Name.php @@ -0,0 +1,90 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Name extends Twig_Node_Expression +{ + protected $specialVars = array( + '_self' => '$this', + '_context' => '$context', + '_charset' => '$this->env->getCharset()', + ); + + public function __construct($name, $lineno) + { + parent::__construct(array(), array('name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $name = $this->getAttribute('name'); + + $compiler->addDebugInfo($this); + + if ($this->getAttribute('is_defined_test')) { + if ($this->isSpecial()) { + $compiler->repr(true); + } else { + $compiler->raw('array_key_exists(')->repr($name)->raw(', $context)'); + } + } elseif ($this->isSpecial()) { + $compiler->raw($this->specialVars[$name]); + } elseif ($this->getAttribute('always_defined')) { + $compiler + ->raw('$context[') + ->string($name) + ->raw(']') + ; + } else { + // remove the non-PHP 5.4 version when PHP 5.3 support is dropped + // as the non-optimized version is just a workaround for slow ternary operator + // when the context has a lot of variables + if (PHP_VERSION_ID >= 50400) { + // PHP 5.4 ternary operator performance was optimized + $compiler + ->raw('(isset($context[') + ->string($name) + ->raw(']) ? $context[') + ->string($name) + ->raw('] : ') + ; + + if ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) { + $compiler->raw('null)'); + } else { + $compiler->raw('$this->getContext($context, ')->string($name)->raw('))'); + } + } else { + $compiler + ->raw('$this->getContext($context, ') + ->string($name) + ; + + if ($this->getAttribute('ignore_strict_check')) { + $compiler->raw(', true'); + } + + $compiler + ->raw(')') + ; + } + } + } + + public function isSpecial() + { + return isset($this->specialVars[$this->getAttribute('name')]); + } + + public function isSimple() + { + return !$this->isSpecial() && !$this->getAttribute('is_defined_test'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/NullCoalesce.php b/public/system/library/template/Twig/Node/Expression/NullCoalesce.php new file mode 100644 index 0000000..1003913 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/NullCoalesce.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_NullCoalesce extends Twig_Node_Expression_Conditional +{ + public function __construct(Twig_NodeInterface $left, Twig_NodeInterface $right, $lineno) + { + $test = new Twig_Node_Expression_Binary_And( + new Twig_Node_Expression_Test_Defined(clone $left, 'defined', new Twig_Node(), $left->getLine()), + new Twig_Node_Expression_Unary_Not(new Twig_Node_Expression_Test_Null($left, 'null', new Twig_Node(), $left->getLine()), $left->getLine()), + $left->getLine() + ); + + parent::__construct($test, $left, $right, $lineno); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Parent.php b/public/system/library/template/Twig/Node/Expression/Parent.php new file mode 100644 index 0000000..694c080 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Parent.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a parent node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Expression_Parent extends Twig_Node_Expression +{ + public function __construct($name, $lineno, $tag = null) + { + parent::__construct(array(), array('output' => false, 'name' => $name), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + if ($this->getAttribute('output')) { + $compiler + ->addDebugInfo($this) + ->write('$this->displayParentBlock(') + ->string($this->getAttribute('name')) + ->raw(", \$context, \$blocks);\n") + ; + } else { + $compiler + ->raw('$this->renderParentBlock(') + ->string($this->getAttribute('name')) + ->raw(', $context, $blocks)') + ; + } + } +} diff --git a/public/system/library/template/Twig/Node/Expression/TempName.php b/public/system/library/template/Twig/Node/Expression/TempName.php new file mode 100644 index 0000000..e6b058e --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/TempName.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_TempName extends Twig_Node_Expression +{ + public function __construct($name, $lineno) + { + parent::__construct(array(), array('name' => $name), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('$_') + ->raw($this->getAttribute('name')) + ->raw('_') + ; + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Test.php b/public/system/library/template/Twig/Node/Expression/Test.php new file mode 100644 index 0000000..c0358c8 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Test.php @@ -0,0 +1,35 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Test extends Twig_Node_Expression_Call +{ + public function __construct(Twig_NodeInterface $node, $name, Twig_NodeInterface $arguments = null, $lineno) + { + parent::__construct(array('node' => $node, 'arguments' => $arguments), array('name' => $name), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $name = $this->getAttribute('name'); + $test = $compiler->getEnvironment()->getTest($name); + + $this->setAttribute('name', $name); + $this->setAttribute('type', 'test'); + $this->setAttribute('thing', $test); + if ($test instanceof Twig_TestCallableInterface || $test instanceof Twig_SimpleTest) { + $this->setAttribute('callable', $test->getCallable()); + } + if ($test instanceof Twig_SimpleTest) { + $this->setAttribute('is_variadic', $test->isVariadic()); + } + + $this->compileCallable($compiler); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Test/Constant.php b/public/system/library/template/Twig/Node/Expression/Test/Constant.php new file mode 100644 index 0000000..de55f5f --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Test/Constant.php @@ -0,0 +1,46 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Checks if a variable is the exact same value as a constant. + * + * <pre> + * {% if post.status is constant('Post::PUBLISHED') %} + * the status attribute is exactly the same as Post::PUBLISHED + * {% endif %} + * </pre> + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Expression_Test_Constant extends Twig_Node_Expression_Test +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('(') + ->subcompile($this->getNode('node')) + ->raw(' === constant(') + ; + + if ($this->getNode('arguments')->hasNode(1)) { + $compiler + ->raw('get_class(') + ->subcompile($this->getNode('arguments')->getNode(1)) + ->raw(')."::".') + ; + } + + $compiler + ->subcompile($this->getNode('arguments')->getNode(0)) + ->raw('))') + ; + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Test/Defined.php b/public/system/library/template/Twig/Node/Expression/Test/Defined.php new file mode 100644 index 0000000..3dc6ff5 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Test/Defined.php @@ -0,0 +1,56 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Checks if a variable is defined in the current context. + * + * <pre> + * {# defined works with variable names and variable attributes #} + * {% if foo is defined %} + * {# ... #} + * {% endif %} + * </pre> + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Expression_Test_Defined extends Twig_Node_Expression_Test +{ + public function __construct(Twig_NodeInterface $node, $name, Twig_NodeInterface $arguments = null, $lineno) + { + if ($node instanceof Twig_Node_Expression_Name) { + $node->setAttribute('is_defined_test', true); + } elseif ($node instanceof Twig_Node_Expression_GetAttr) { + $node->setAttribute('is_defined_test', true); + + $this->changeIgnoreStrictCheck($node); + } elseif ($node instanceof Twig_Node_Expression_Constant || $node instanceof Twig_Node_Expression_Array) { + $node = new Twig_Node_Expression_Constant(true, $node->getLine()); + } else { + throw new Twig_Error_Syntax('The "defined" test only works with simple variables.', $this->getLine()); + } + + parent::__construct($node, $name, $arguments, $lineno); + } + + protected function changeIgnoreStrictCheck(Twig_Node_Expression_GetAttr $node) + { + $node->setAttribute('ignore_strict_check', true); + + if ($node->getNode('node') instanceof Twig_Node_Expression_GetAttr) { + $this->changeIgnoreStrictCheck($node->getNode('node')); + } + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->subcompile($this->getNode('node')); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Test/Divisibleby.php b/public/system/library/template/Twig/Node/Expression/Test/Divisibleby.php new file mode 100644 index 0000000..d5bed23 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Test/Divisibleby.php @@ -0,0 +1,33 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Checks if a variable is divisible by a number. + * + * <pre> + * {% if loop.index is divisible by(3) %} + * </pre> + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Expression_Test_Divisibleby extends Twig_Node_Expression_Test +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('(0 == ') + ->subcompile($this->getNode('node')) + ->raw(' % ') + ->subcompile($this->getNode('arguments')->getNode(0)) + ->raw(')') + ; + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Test/Even.php b/public/system/library/template/Twig/Node/Expression/Test/Even.php new file mode 100644 index 0000000..d7853e8 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Test/Even.php @@ -0,0 +1,32 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Checks if a number is even. + * + * <pre> + * {{ var is even }} + * </pre> + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Expression_Test_Even extends Twig_Node_Expression_Test +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('(') + ->subcompile($this->getNode('node')) + ->raw(' % 2 == 0') + ->raw(')') + ; + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Test/Null.php b/public/system/library/template/Twig/Node/Expression/Test/Null.php new file mode 100644 index 0000000..1c83825 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Test/Null.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Checks that a variable is null. + * + * <pre> + * {{ var is none }} + * </pre> + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Expression_Test_Null extends Twig_Node_Expression_Test +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('(null === ') + ->subcompile($this->getNode('node')) + ->raw(')') + ; + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Test/Odd.php b/public/system/library/template/Twig/Node/Expression/Test/Odd.php new file mode 100644 index 0000000..421c19e --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Test/Odd.php @@ -0,0 +1,32 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Checks if a number is odd. + * + * <pre> + * {{ var is odd }} + * </pre> + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Expression_Test_Odd extends Twig_Node_Expression_Test +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('(') + ->subcompile($this->getNode('node')) + ->raw(' % 2 == 1') + ->raw(')') + ; + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Test/Sameas.php b/public/system/library/template/Twig/Node/Expression/Test/Sameas.php new file mode 100644 index 0000000..b48905e --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Test/Sameas.php @@ -0,0 +1,29 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Checks if a variable is the same as another one (=== in PHP). + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Expression_Test_Sameas extends Twig_Node_Expression_Test +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('(') + ->subcompile($this->getNode('node')) + ->raw(' === ') + ->subcompile($this->getNode('arguments')->getNode(0)) + ->raw(')') + ; + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Unary.php b/public/system/library/template/Twig/Node/Expression/Unary.php new file mode 100644 index 0000000..1cf54c3 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Unary.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +abstract class Twig_Node_Expression_Unary extends Twig_Node_Expression +{ + public function __construct(Twig_NodeInterface $node, $lineno) + { + parent::__construct(array('node' => $node), array(), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->raw(' '); + $this->operator($compiler); + $compiler->subcompile($this->getNode('node')); + } + + abstract public function operator(Twig_Compiler $compiler); +} diff --git a/public/system/library/template/Twig/Node/Expression/Unary/Neg.php b/public/system/library/template/Twig/Node/Expression/Unary/Neg.php new file mode 100644 index 0000000..2a3937e --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Unary/Neg.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Unary_Neg extends Twig_Node_Expression_Unary +{ + public function operator(Twig_Compiler $compiler) + { + $compiler->raw('-'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Unary/Not.php b/public/system/library/template/Twig/Node/Expression/Unary/Not.php new file mode 100644 index 0000000..f94073c --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Unary/Not.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Unary_Not extends Twig_Node_Expression_Unary +{ + public function operator(Twig_Compiler $compiler) + { + $compiler->raw('!'); + } +} diff --git a/public/system/library/template/Twig/Node/Expression/Unary/Pos.php b/public/system/library/template/Twig/Node/Expression/Unary/Pos.php new file mode 100644 index 0000000..04edb52 --- /dev/null +++ b/public/system/library/template/Twig/Node/Expression/Unary/Pos.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +class Twig_Node_Expression_Unary_Pos extends Twig_Node_Expression_Unary +{ + public function operator(Twig_Compiler $compiler) + { + $compiler->raw('+'); + } +} diff --git a/public/system/library/template/Twig/Node/Flush.php b/public/system/library/template/Twig/Node/Flush.php new file mode 100644 index 0000000..2af17a4 --- /dev/null +++ b/public/system/library/template/Twig/Node/Flush.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a flush node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Flush extends Twig_Node +{ + public function __construct($lineno, $tag) + { + parent::__construct(array(), array(), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write("flush();\n") + ; + } +} diff --git a/public/system/library/template/Twig/Node/For.php b/public/system/library/template/Twig/Node/For.php new file mode 100644 index 0000000..2d45093 --- /dev/null +++ b/public/system/library/template/Twig/Node/For.php @@ -0,0 +1,106 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a for node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_For extends Twig_Node +{ + protected $loop; + + public function __construct(Twig_Node_Expression_AssignName $keyTarget, Twig_Node_Expression_AssignName $valueTarget, Twig_Node_Expression $seq, Twig_Node_Expression $ifexpr = null, Twig_NodeInterface $body, Twig_NodeInterface $else = null, $lineno, $tag = null) + { + $body = new Twig_Node(array($body, $this->loop = new Twig_Node_ForLoop($lineno, $tag))); + + if (null !== $ifexpr) { + $body = new Twig_Node_If(new Twig_Node(array($ifexpr, $body)), null, $lineno, $tag); + } + + parent::__construct(array('key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body, 'else' => $else), array('with_loop' => true, 'ifexpr' => null !== $ifexpr), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write("\$context['_parent'] = \$context;\n") + ->write("\$context['_seq'] = twig_ensure_traversable(") + ->subcompile($this->getNode('seq')) + ->raw(");\n") + ; + + if (null !== $this->getNode('else')) { + $compiler->write("\$context['_iterated'] = false;\n"); + } + + if ($this->getAttribute('with_loop')) { + $compiler + ->write("\$context['loop'] = array(\n") + ->write(" 'parent' => \$context['_parent'],\n") + ->write(" 'index0' => 0,\n") + ->write(" 'index' => 1,\n") + ->write(" 'first' => true,\n") + ->write(");\n") + ; + + if (!$this->getAttribute('ifexpr')) { + $compiler + ->write("if (is_array(\$context['_seq']) || (is_object(\$context['_seq']) && \$context['_seq'] instanceof Countable)) {\n") + ->indent() + ->write("\$length = count(\$context['_seq']);\n") + ->write("\$context['loop']['revindex0'] = \$length - 1;\n") + ->write("\$context['loop']['revindex'] = \$length;\n") + ->write("\$context['loop']['length'] = \$length;\n") + ->write("\$context['loop']['last'] = 1 === \$length;\n") + ->outdent() + ->write("}\n") + ; + } + } + + $this->loop->setAttribute('else', null !== $this->getNode('else')); + $this->loop->setAttribute('with_loop', $this->getAttribute('with_loop')); + $this->loop->setAttribute('ifexpr', $this->getAttribute('ifexpr')); + + $compiler + ->write("foreach (\$context['_seq'] as ") + ->subcompile($this->getNode('key_target')) + ->raw(' => ') + ->subcompile($this->getNode('value_target')) + ->raw(") {\n") + ->indent() + ->subcompile($this->getNode('body')) + ->outdent() + ->write("}\n") + ; + + if (null !== $this->getNode('else')) { + $compiler + ->write("if (!\$context['_iterated']) {\n") + ->indent() + ->subcompile($this->getNode('else')) + ->outdent() + ->write("}\n") + ; + } + + $compiler->write("\$_parent = \$context['_parent'];\n"); + + // remove some "private" loop variables (needed for nested loops) + $compiler->write('unset($context[\'_seq\'], $context[\'_iterated\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\'], $context[\'loop\']);'."\n"); + + // keep the values set in the inner context for variables defined in the outer context + $compiler->write("\$context = array_intersect_key(\$context, \$_parent) + \$_parent;\n"); + } +} diff --git a/public/system/library/template/Twig/Node/ForLoop.php b/public/system/library/template/Twig/Node/ForLoop.php new file mode 100644 index 0000000..2554d48 --- /dev/null +++ b/public/system/library/template/Twig/Node/ForLoop.php @@ -0,0 +1,50 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Internal node used by the for node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_ForLoop extends Twig_Node +{ + public function __construct($lineno, $tag = null) + { + parent::__construct(array(), array('with_loop' => false, 'ifexpr' => false, 'else' => false), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + if ($this->getAttribute('else')) { + $compiler->write("\$context['_iterated'] = true;\n"); + } + + if ($this->getAttribute('with_loop')) { + $compiler + ->write("++\$context['loop']['index0'];\n") + ->write("++\$context['loop']['index'];\n") + ->write("\$context['loop']['first'] = false;\n") + ; + + if (!$this->getAttribute('ifexpr')) { + $compiler + ->write("if (isset(\$context['loop']['length'])) {\n") + ->indent() + ->write("--\$context['loop']['revindex0'];\n") + ->write("--\$context['loop']['revindex'];\n") + ->write("\$context['loop']['last'] = 0 === \$context['loop']['revindex0'];\n") + ->outdent() + ->write("}\n") + ; + } + } + } +} diff --git a/public/system/library/template/Twig/Node/If.php b/public/system/library/template/Twig/Node/If.php new file mode 100644 index 0000000..caff936 --- /dev/null +++ b/public/system/library/template/Twig/Node/If.php @@ -0,0 +1,61 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents an if node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_If extends Twig_Node +{ + public function __construct(Twig_NodeInterface $tests, Twig_NodeInterface $else = null, $lineno, $tag = null) + { + parent::__construct(array('tests' => $tests, 'else' => $else), array(), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->addDebugInfo($this); + for ($i = 0, $count = count($this->getNode('tests')); $i < $count; $i += 2) { + if ($i > 0) { + $compiler + ->outdent() + ->write('} elseif (') + ; + } else { + $compiler + ->write('if (') + ; + } + + $compiler + ->subcompile($this->getNode('tests')->getNode($i)) + ->raw(") {\n") + ->indent() + ->subcompile($this->getNode('tests')->getNode($i + 1)) + ; + } + + if ($this->hasNode('else') && null !== $this->getNode('else')) { + $compiler + ->outdent() + ->write("} else {\n") + ->indent() + ->subcompile($this->getNode('else')) + ; + } + + $compiler + ->outdent() + ->write("}\n"); + } +} diff --git a/public/system/library/template/Twig/Node/Import.php b/public/system/library/template/Twig/Node/Import.php new file mode 100644 index 0000000..df37af3 --- /dev/null +++ b/public/system/library/template/Twig/Node/Import.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents an import node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Import extends Twig_Node +{ + public function __construct(Twig_Node_Expression $expr, Twig_Node_Expression $var, $lineno, $tag = null) + { + parent::__construct(array('expr' => $expr, 'var' => $var), array(), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('') + ->subcompile($this->getNode('var')) + ->raw(' = ') + ; + + if ($this->getNode('expr') instanceof Twig_Node_Expression_Name && '_self' === $this->getNode('expr')->getAttribute('name')) { + $compiler->raw('$this'); + } else { + $compiler + ->raw('$this->loadTemplate(') + ->subcompile($this->getNode('expr')) + ->raw(', ') + ->repr($compiler->getFilename()) + ->raw(', ') + ->repr($this->getLine()) + ->raw(')') + ; + } + + $compiler->raw(";\n"); + } +} diff --git a/public/system/library/template/Twig/Node/Include.php b/public/system/library/template/Twig/Node/Include.php new file mode 100644 index 0000000..9952f73 --- /dev/null +++ b/public/system/library/template/Twig/Node/Include.php @@ -0,0 +1,83 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents an include node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Include extends Twig_Node implements Twig_NodeOutputInterface +{ + public function __construct(Twig_Node_Expression $expr, Twig_Node_Expression $variables = null, $only = false, $ignoreMissing = false, $lineno, $tag = null) + { + parent::__construct(array('expr' => $expr, 'variables' => $variables), array('only' => (bool) $only, 'ignore_missing' => (bool) $ignoreMissing), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->addDebugInfo($this); + + if ($this->getAttribute('ignore_missing')) { + $compiler + ->write("try {\n") + ->indent() + ; + } + + $this->addGetTemplate($compiler); + + $compiler->raw('->display('); + + $this->addTemplateArguments($compiler); + + $compiler->raw(");\n"); + + if ($this->getAttribute('ignore_missing')) { + $compiler + ->outdent() + ->write("} catch (Twig_Error_Loader \$e) {\n") + ->indent() + ->write("// ignore missing template\n") + ->outdent() + ->write("}\n\n") + ; + } + } + + protected function addGetTemplate(Twig_Compiler $compiler) + { + $compiler + ->write('$this->loadTemplate(') + ->subcompile($this->getNode('expr')) + ->raw(', ') + ->repr($compiler->getFilename()) + ->raw(', ') + ->repr($this->getLine()) + ->raw(')') + ; + } + + protected function addTemplateArguments(Twig_Compiler $compiler) + { + if (null === $this->getNode('variables')) { + $compiler->raw(false === $this->getAttribute('only') ? '$context' : 'array()'); + } elseif (false === $this->getAttribute('only')) { + $compiler + ->raw('array_merge($context, ') + ->subcompile($this->getNode('variables')) + ->raw(')') + ; + } else { + $compiler->subcompile($this->getNode('variables')); + } + } +} diff --git a/public/system/library/template/Twig/Node/Macro.php b/public/system/library/template/Twig/Node/Macro.php new file mode 100644 index 0000000..c186cd0 --- /dev/null +++ b/public/system/library/template/Twig/Node/Macro.php @@ -0,0 +1,123 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a macro node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Macro extends Twig_Node +{ + const VARARGS_NAME = 'varargs'; + + public function __construct($name, Twig_NodeInterface $body, Twig_NodeInterface $arguments, $lineno, $tag = null) + { + foreach ($arguments as $argumentName => $argument) { + if (self::VARARGS_NAME === $argumentName) { + throw new Twig_Error_Syntax(sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getLine()); + } + } + + parent::__construct(array('body' => $body, 'arguments' => $arguments), array('name' => $name), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write(sprintf('public function get%s(', $this->getAttribute('name'))) + ; + + $count = count($this->getNode('arguments')); + $pos = 0; + foreach ($this->getNode('arguments') as $name => $default) { + $compiler + ->raw('$__'.$name.'__ = ') + ->subcompile($default) + ; + + if (++$pos < $count) { + $compiler->raw(', '); + } + } + + if (PHP_VERSION_ID >= 50600) { + if ($count) { + $compiler->raw(', '); + } + + $compiler->raw('...$__varargs__'); + } + + $compiler + ->raw(")\n") + ->write("{\n") + ->indent() + ; + + $compiler + ->write("\$context = \$this->env->mergeGlobals(array(\n") + ->indent() + ; + + foreach ($this->getNode('arguments') as $name => $default) { + $compiler + ->addIndentation() + ->string($name) + ->raw(' => $__'.$name.'__') + ->raw(",\n") + ; + } + + $compiler + ->addIndentation() + ->string(self::VARARGS_NAME) + ->raw(' => ') + ; + + if (PHP_VERSION_ID >= 50600) { + $compiler->raw("\$__varargs__,\n"); + } else { + $compiler + ->raw('func_num_args() > ') + ->repr($count) + ->raw(' ? array_slice(func_get_args(), ') + ->repr($count) + ->raw(") : array(),\n") + ; + } + + $compiler + ->outdent() + ->write("));\n\n") + ->write("\$blocks = array();\n\n") + ->write("ob_start();\n") + ->write("try {\n") + ->indent() + ->subcompile($this->getNode('body')) + ->outdent() + ->write("} catch (Exception \$e) {\n") + ->indent() + ->write("ob_end_clean();\n\n") + ->write("throw \$e;\n") + ->outdent() + ->write("} catch (Throwable \$e) {\n") + ->indent() + ->write("ob_end_clean();\n\n") + ->write("throw \$e;\n") + ->outdent() + ->write("}\n\n") + ->write("return ('' === \$tmp = ob_get_clean()) ? '' : new Twig_Markup(\$tmp, \$this->env->getCharset());\n") + ->outdent() + ->write("}\n\n") + ; + } +} diff --git a/public/system/library/template/Twig/Node/Module.php b/public/system/library/template/Twig/Node/Module.php new file mode 100644 index 0000000..01161d3 --- /dev/null +++ b/public/system/library/template/Twig/Node/Module.php @@ -0,0 +1,403 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a module node. + * + * Consider this class as being final. If you need to customize the behavior of + * the generated class, consider adding nodes to the following nodes: display_start, + * display_end, constructor_start, constructor_end, and class_end. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Module extends Twig_Node +{ + public function __construct(Twig_NodeInterface $body, Twig_Node_Expression $parent = null, Twig_NodeInterface $blocks, Twig_NodeInterface $macros, Twig_NodeInterface $traits, $embeddedTemplates, $filename) + { + // embedded templates are set as attributes so that they are only visited once by the visitors + parent::__construct(array( + 'parent' => $parent, + 'body' => $body, + 'blocks' => $blocks, + 'macros' => $macros, + 'traits' => $traits, + 'display_start' => new Twig_Node(), + 'display_end' => new Twig_Node(), + 'constructor_start' => new Twig_Node(), + 'constructor_end' => new Twig_Node(), + 'class_end' => new Twig_Node(), + ), array( + 'filename' => $filename, + 'index' => null, + 'embedded_templates' => $embeddedTemplates, + ), 1); + } + + public function setIndex($index) + { + $this->setAttribute('index', $index); + } + + public function compile(Twig_Compiler $compiler) + { + $this->compileTemplate($compiler); + + foreach ($this->getAttribute('embedded_templates') as $template) { + $compiler->subcompile($template); + } + } + + protected function compileTemplate(Twig_Compiler $compiler) + { + if (!$this->getAttribute('index')) { + $compiler->write('<?php'); + } + + $this->compileClassHeader($compiler); + + if ( + count($this->getNode('blocks')) + || count($this->getNode('traits')) + || null === $this->getNode('parent') + || $this->getNode('parent') instanceof Twig_Node_Expression_Constant + || count($this->getNode('constructor_start')) + || count($this->getNode('constructor_end')) + ) { + $this->compileConstructor($compiler); + } + + $this->compileGetParent($compiler); + + $this->compileDisplay($compiler); + + $compiler->subcompile($this->getNode('blocks')); + + $this->compileMacros($compiler); + + $this->compileGetTemplateName($compiler); + + $this->compileIsTraitable($compiler); + + $this->compileDebugInfo($compiler); + + $this->compileClassFooter($compiler); + } + + protected function compileGetParent(Twig_Compiler $compiler) + { + if (null === $parent = $this->getNode('parent')) { + return; + } + + $compiler + ->write("protected function doGetParent(array \$context)\n", "{\n") + ->indent() + ->addDebugInfo($parent) + ->write('return ') + ; + + if ($parent instanceof Twig_Node_Expression_Constant) { + $compiler->subcompile($parent); + } else { + $compiler + ->raw('$this->loadTemplate(') + ->subcompile($parent) + ->raw(', ') + ->repr($compiler->getFilename()) + ->raw(', ') + ->repr($this->getNode('parent')->getLine()) + ->raw(')') + ; + } + + $compiler + ->raw(";\n") + ->outdent() + ->write("}\n\n") + ; + } + + protected function compileClassHeader(Twig_Compiler $compiler) + { + $compiler + ->write("\n\n") + // if the filename contains */, add a blank to avoid a PHP parse error + ->write('/* '.str_replace('*/', '* /', $this->getAttribute('filename'))." */\n") + ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getAttribute('filename'), $this->getAttribute('index'))) + ->raw(sprintf(" extends %s\n", $compiler->getEnvironment()->getBaseTemplateClass())) + ->write("{\n") + ->indent() + ; + } + + protected function compileConstructor(Twig_Compiler $compiler) + { + $compiler + ->write("public function __construct(Twig_Environment \$env)\n", "{\n") + ->indent() + ->subcompile($this->getNode('constructor_start')) + ->write("parent::__construct(\$env);\n\n") + ; + + // parent + if (null === $parent = $this->getNode('parent')) { + $compiler->write("\$this->parent = false;\n\n"); + } elseif ($parent instanceof Twig_Node_Expression_Constant) { + $compiler + ->addDebugInfo($parent) + ->write('$this->parent = $this->loadTemplate(') + ->subcompile($parent) + ->raw(', ') + ->repr($compiler->getFilename()) + ->raw(', ') + ->repr($this->getNode('parent')->getLine()) + ->raw(");\n") + ; + } + + $countTraits = count($this->getNode('traits')); + if ($countTraits) { + // traits + foreach ($this->getNode('traits') as $i => $trait) { + $this->compileLoadTemplate($compiler, $trait->getNode('template'), sprintf('$_trait_%s', $i)); + + $compiler + ->addDebugInfo($trait->getNode('template')) + ->write(sprintf("if (!\$_trait_%s->isTraitable()) {\n", $i)) + ->indent() + ->write("throw new Twig_Error_Runtime('Template \"'.") + ->subcompile($trait->getNode('template')) + ->raw(".'\" cannot be used as a trait.');\n") + ->outdent() + ->write("}\n") + ->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->getBlocks();\n\n", $i, $i)) + ; + + foreach ($trait->getNode('targets') as $key => $value) { + $compiler + ->write(sprintf('if (!isset($_trait_%s_blocks[', $i)) + ->string($key) + ->raw("])) {\n") + ->indent() + ->write("throw new Twig_Error_Runtime(sprintf('Block ") + ->string($key) + ->raw(' is not defined in trait ') + ->subcompile($trait->getNode('template')) + ->raw(".'));\n") + ->outdent() + ->write("}\n\n") + + ->write(sprintf('$_trait_%s_blocks[', $i)) + ->subcompile($value) + ->raw(sprintf('] = $_trait_%s_blocks[', $i)) + ->string($key) + ->raw(sprintf(']; unset($_trait_%s_blocks[', $i)) + ->string($key) + ->raw("]);\n\n") + ; + } + } + + if ($countTraits > 1) { + $compiler + ->write("\$this->traits = array_merge(\n") + ->indent() + ; + + for ($i = 0; $i < $countTraits; ++$i) { + $compiler + ->write(sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i)) + ; + } + + $compiler + ->outdent() + ->write(");\n\n") + ; + } else { + $compiler + ->write("\$this->traits = \$_trait_0_blocks;\n\n") + ; + } + + $compiler + ->write("\$this->blocks = array_merge(\n") + ->indent() + ->write("\$this->traits,\n") + ->write("array(\n") + ; + } else { + $compiler + ->write("\$this->blocks = array(\n") + ; + } + + // blocks + $compiler + ->indent() + ; + + foreach ($this->getNode('blocks') as $name => $node) { + $compiler + ->write(sprintf("'%s' => array(\$this, 'block_%s'),\n", $name, $name)) + ; + } + + if ($countTraits) { + $compiler + ->outdent() + ->write(")\n") + ; + } + + $compiler + ->outdent() + ->write(");\n") + ->outdent() + ->subcompile($this->getNode('constructor_end')) + ->write("}\n\n") + ; + } + + protected function compileDisplay(Twig_Compiler $compiler) + { + $compiler + ->write("protected function doDisplay(array \$context, array \$blocks = array())\n", "{\n") + ->indent() + ->subcompile($this->getNode('display_start')) + ->subcompile($this->getNode('body')) + ; + + if (null !== $parent = $this->getNode('parent')) { + $compiler->addDebugInfo($parent); + if ($parent instanceof Twig_Node_Expression_Constant) { + $compiler->write('$this->parent'); + } else { + $compiler->write('$this->getParent($context)'); + } + $compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n"); + } + + $compiler + ->subcompile($this->getNode('display_end')) + ->outdent() + ->write("}\n\n") + ; + } + + protected function compileClassFooter(Twig_Compiler $compiler) + { + $compiler + ->subcompile($this->getNode('class_end')) + ->outdent() + ->write("}\n") + ; + } + + protected function compileMacros(Twig_Compiler $compiler) + { + $compiler->subcompile($this->getNode('macros')); + } + + protected function compileGetTemplateName(Twig_Compiler $compiler) + { + $compiler + ->write("public function getTemplateName()\n", "{\n") + ->indent() + ->write('return ') + ->repr($this->getAttribute('filename')) + ->raw(";\n") + ->outdent() + ->write("}\n\n") + ; + } + + protected function compileIsTraitable(Twig_Compiler $compiler) + { + // A template can be used as a trait if: + // * it has no parent + // * it has no macros + // * it has no body + // + // Put another way, a template can be used as a trait if it + // only contains blocks and use statements. + $traitable = null === $this->getNode('parent') && 0 === count($this->getNode('macros')); + if ($traitable) { + if ($this->getNode('body') instanceof Twig_Node_Body) { + $nodes = $this->getNode('body')->getNode(0); + } else { + $nodes = $this->getNode('body'); + } + + if (!count($nodes)) { + $nodes = new Twig_Node(array($nodes)); + } + + foreach ($nodes as $node) { + if (!count($node)) { + continue; + } + + if ($node instanceof Twig_Node_Text && ctype_space($node->getAttribute('data'))) { + continue; + } + + if ($node instanceof Twig_Node_BlockReference) { + continue; + } + + $traitable = false; + break; + } + } + + if ($traitable) { + return; + } + + $compiler + ->write("public function isTraitable()\n", "{\n") + ->indent() + ->write(sprintf("return %s;\n", $traitable ? 'true' : 'false')) + ->outdent() + ->write("}\n\n") + ; + } + + protected function compileDebugInfo(Twig_Compiler $compiler) + { + $compiler + ->write("public function getDebugInfo()\n", "{\n") + ->indent() + ->write(sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) + ->outdent() + ->write("}\n") + ; + } + + protected function compileLoadTemplate(Twig_Compiler $compiler, $node, $var) + { + if ($node instanceof Twig_Node_Expression_Constant) { + $compiler + ->write(sprintf('%s = $this->loadTemplate(', $var)) + ->subcompile($node) + ->raw(', ') + ->repr($compiler->getFilename()) + ->raw(', ') + ->repr($node->getLine()) + ->raw(");\n") + ; + } else { + throw new LogicException('Trait templates can only be constant nodes'); + } + } +} diff --git a/public/system/library/template/Twig/Node/Print.php b/public/system/library/template/Twig/Node/Print.php new file mode 100644 index 0000000..7b69ee8 --- /dev/null +++ b/public/system/library/template/Twig/Node/Print.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a node that outputs an expression. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Print extends Twig_Node implements Twig_NodeOutputInterface +{ + public function __construct(Twig_Node_Expression $expr, $lineno, $tag = null) + { + parent::__construct(array('expr' => $expr), array(), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('echo ') + ->subcompile($this->getNode('expr')) + ->raw(";\n") + ; + } +} diff --git a/public/system/library/template/Twig/Node/Sandbox.php b/public/system/library/template/Twig/Node/Sandbox.php new file mode 100644 index 0000000..cd705e2 --- /dev/null +++ b/public/system/library/template/Twig/Node/Sandbox.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a sandbox node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Sandbox extends Twig_Node +{ + public function __construct(Twig_NodeInterface $body, $lineno, $tag = null) + { + parent::__construct(array('body' => $body), array(), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write("\$sandbox = \$this->env->getExtension('sandbox');\n") + ->write("if (!\$alreadySandboxed = \$sandbox->isSandboxed()) {\n") + ->indent() + ->write("\$sandbox->enableSandbox();\n") + ->outdent() + ->write("}\n") + ->subcompile($this->getNode('body')) + ->write("if (!\$alreadySandboxed) {\n") + ->indent() + ->write("\$sandbox->disableSandbox();\n") + ->outdent() + ->write("}\n") + ; + } +} diff --git a/public/system/library/template/Twig/Node/SandboxedPrint.php b/public/system/library/template/Twig/Node/SandboxedPrint.php new file mode 100644 index 0000000..148dd2b --- /dev/null +++ b/public/system/library/template/Twig/Node/SandboxedPrint.php @@ -0,0 +1,51 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Twig_Node_SandboxedPrint adds a check for the __toString() method + * when the variable is an object and the sandbox is activated. + * + * When there is a simple Print statement, like {{ article }}, + * and if the sandbox is enabled, we need to check that the __toString() + * method is allowed if 'article' is an object. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_SandboxedPrint extends Twig_Node_Print +{ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('echo $this->env->getExtension(\'sandbox\')->ensureToStringAllowed(') + ->subcompile($this->getNode('expr')) + ->raw(");\n") + ; + } + + /** + * Removes node filters. + * + * This is mostly needed when another visitor adds filters (like the escaper one). + * + * @param Twig_Node $node A Node + * + * @return Twig_Node + */ + protected function removeNodeFilter($node) + { + if ($node instanceof Twig_Node_Expression_Filter) { + return $this->removeNodeFilter($node->getNode('node')); + } + + return $node; + } +} diff --git a/public/system/library/template/Twig/Node/Set.php b/public/system/library/template/Twig/Node/Set.php new file mode 100644 index 0000000..e5a6603 --- /dev/null +++ b/public/system/library/template/Twig/Node/Set.php @@ -0,0 +1,96 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a set node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Set extends Twig_Node +{ + public function __construct($capture, Twig_NodeInterface $names, Twig_NodeInterface $values, $lineno, $tag = null) + { + parent::__construct(array('names' => $names, 'values' => $values), array('capture' => $capture, 'safe' => false), $lineno, $tag); + + /* + * Optimizes the node when capture is used for a large block of text. + * + * {% set foo %}foo{% endset %} is compiled to $context['foo'] = new Twig_Markup("foo"); + */ + if ($this->getAttribute('capture')) { + $this->setAttribute('safe', true); + + $values = $this->getNode('values'); + if ($values instanceof Twig_Node_Text) { + $this->setNode('values', new Twig_Node_Expression_Constant($values->getAttribute('data'), $values->getLine())); + $this->setAttribute('capture', false); + } + } + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->addDebugInfo($this); + + if (count($this->getNode('names')) > 1) { + $compiler->write('list('); + foreach ($this->getNode('names') as $idx => $node) { + if ($idx) { + $compiler->raw(', '); + } + + $compiler->subcompile($node); + } + $compiler->raw(')'); + } else { + if ($this->getAttribute('capture')) { + $compiler + ->write("ob_start();\n") + ->subcompile($this->getNode('values')) + ; + } + + $compiler->subcompile($this->getNode('names'), false); + + if ($this->getAttribute('capture')) { + $compiler->raw(" = ('' === \$tmp = ob_get_clean()) ? '' : new Twig_Markup(\$tmp, \$this->env->getCharset())"); + } + } + + if (!$this->getAttribute('capture')) { + $compiler->raw(' = '); + + if (count($this->getNode('names')) > 1) { + $compiler->write('array('); + foreach ($this->getNode('values') as $idx => $value) { + if ($idx) { + $compiler->raw(', '); + } + + $compiler->subcompile($value); + } + $compiler->raw(')'); + } else { + if ($this->getAttribute('safe')) { + $compiler + ->raw("('' === \$tmp = ") + ->subcompile($this->getNode('values')) + ->raw(") ? '' : new Twig_Markup(\$tmp, \$this->env->getCharset())") + ; + } else { + $compiler->subcompile($this->getNode('values')); + } + } + } + + $compiler->raw(";\n"); + } +} diff --git a/public/system/library/template/Twig/Node/SetTemp.php b/public/system/library/template/Twig/Node/SetTemp.php new file mode 100644 index 0000000..3bdd1cb --- /dev/null +++ b/public/system/library/template/Twig/Node/SetTemp.php @@ -0,0 +1,35 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +class Twig_Node_SetTemp extends Twig_Node +{ + public function __construct($name, $lineno) + { + parent::__construct(array(), array('name' => $name), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $name = $this->getAttribute('name'); + $compiler + ->addDebugInfo($this) + ->write('if (isset($context[') + ->string($name) + ->raw('])) { $_') + ->raw($name) + ->raw('_ = $context[') + ->repr($name) + ->raw(']; } else { $_') + ->raw($name) + ->raw("_ = null; }\n") + ; + } +} diff --git a/public/system/library/template/Twig/Node/Spaceless.php b/public/system/library/template/Twig/Node/Spaceless.php new file mode 100644 index 0000000..486e461 --- /dev/null +++ b/public/system/library/template/Twig/Node/Spaceless.php @@ -0,0 +1,35 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a spaceless node. + * + * It removes spaces between HTML tags. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Spaceless extends Twig_Node +{ + public function __construct(Twig_NodeInterface $body, $lineno, $tag = 'spaceless') + { + parent::__construct(array('body' => $body), array(), $lineno, $tag); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write("ob_start();\n") + ->subcompile($this->getNode('body')) + ->write("echo trim(preg_replace('/>\s+</', '><', ob_get_clean()));\n") + ; + } +} diff --git a/public/system/library/template/Twig/Node/Text.php b/public/system/library/template/Twig/Node/Text.php new file mode 100644 index 0000000..39879bb --- /dev/null +++ b/public/system/library/template/Twig/Node/Text.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a text node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Node_Text extends Twig_Node implements Twig_NodeOutputInterface +{ + public function __construct($data, $lineno) + { + parent::__construct(array(), array('data' => $data), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('echo ') + ->string($this->getAttribute('data')) + ->raw(";\n") + ; + } +} diff --git a/public/system/library/template/Twig/NodeInterface.php b/public/system/library/template/Twig/NodeInterface.php new file mode 100644 index 0000000..8077349 --- /dev/null +++ b/public/system/library/template/Twig/NodeInterface.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a node in the AST. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 3.0) + */ +interface Twig_NodeInterface extends Countable, IteratorAggregate +{ + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler); + + public function getLine(); + + public function getNodeTag(); +} diff --git a/public/system/library/template/Twig/NodeOutputInterface.php b/public/system/library/template/Twig/NodeOutputInterface.php new file mode 100644 index 0000000..22172c0 --- /dev/null +++ b/public/system/library/template/Twig/NodeOutputInterface.php @@ -0,0 +1,19 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a displayable node in the AST. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +interface Twig_NodeOutputInterface +{ +} diff --git a/public/system/library/template/Twig/NodeTraverser.php b/public/system/library/template/Twig/NodeTraverser.php new file mode 100644 index 0000000..00f7b54 --- /dev/null +++ b/public/system/library/template/Twig/NodeTraverser.php @@ -0,0 +1,89 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Twig_NodeTraverser is a node traverser. + * + * It visits all nodes and their children and calls the given visitor for each. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_NodeTraverser +{ + protected $env; + protected $visitors = array(); + + /** + * Constructor. + * + * @param Twig_Environment $env A Twig_Environment instance + * @param Twig_NodeVisitorInterface[] $visitors An array of Twig_NodeVisitorInterface instances + */ + public function __construct(Twig_Environment $env, array $visitors = array()) + { + $this->env = $env; + foreach ($visitors as $visitor) { + $this->addVisitor($visitor); + } + } + + /** + * Adds a visitor. + * + * @param Twig_NodeVisitorInterface $visitor A Twig_NodeVisitorInterface instance + */ + public function addVisitor(Twig_NodeVisitorInterface $visitor) + { + if (!isset($this->visitors[$visitor->getPriority()])) { + $this->visitors[$visitor->getPriority()] = array(); + } + + $this->visitors[$visitor->getPriority()][] = $visitor; + } + + /** + * Traverses a node and calls the registered visitors. + * + * @param Twig_NodeInterface $node A Twig_NodeInterface instance + * + * @return Twig_NodeInterface + */ + public function traverse(Twig_NodeInterface $node) + { + ksort($this->visitors); + foreach ($this->visitors as $visitors) { + foreach ($visitors as $visitor) { + $node = $this->traverseForVisitor($visitor, $node); + } + } + + return $node; + } + + protected function traverseForVisitor(Twig_NodeVisitorInterface $visitor, Twig_NodeInterface $node = null) + { + if (null === $node) { + return; + } + + $node = $visitor->enterNode($node, $this->env); + + foreach ($node as $k => $n) { + if (false !== $n = $this->traverseForVisitor($visitor, $n)) { + $node->setNode($k, $n); + } else { + $node->removeNode($k); + } + } + + return $visitor->leaveNode($node, $this->env); + } +} diff --git a/public/system/library/template/Twig/NodeVisitor/Escaper.php b/public/system/library/template/Twig/NodeVisitor/Escaper.php new file mode 100644 index 0000000..f7d01b3 --- /dev/null +++ b/public/system/library/template/Twig/NodeVisitor/Escaper.php @@ -0,0 +1,159 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Twig_NodeVisitor_Escaper implements output escaping. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_NodeVisitor_Escaper extends Twig_BaseNodeVisitor +{ + protected $statusStack = array(); + protected $blocks = array(); + protected $safeAnalysis; + protected $traverser; + protected $defaultStrategy = false; + protected $safeVars = array(); + + public function __construct() + { + $this->safeAnalysis = new Twig_NodeVisitor_SafeAnalysis(); + } + + /** + * {@inheritdoc} + */ + protected function doEnterNode(Twig_Node $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_Module) { + if ($env->hasExtension('escaper') && $defaultStrategy = $env->getExtension('escaper')->getDefaultStrategy($node->getAttribute('filename'))) { + $this->defaultStrategy = $defaultStrategy; + } + $this->safeVars = array(); + $this->blocks = array(); + } elseif ($node instanceof Twig_Node_AutoEscape) { + $this->statusStack[] = $node->getAttribute('value'); + } elseif ($node instanceof Twig_Node_Block) { + $this->statusStack[] = isset($this->blocks[$node->getAttribute('name')]) ? $this->blocks[$node->getAttribute('name')] : $this->needEscaping($env); + } elseif ($node instanceof Twig_Node_Import) { + $this->safeVars[] = $node->getNode('var')->getAttribute('name'); + } + + return $node; + } + + /** + * {@inheritdoc} + */ + protected function doLeaveNode(Twig_Node $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_Module) { + $this->defaultStrategy = false; + $this->safeVars = array(); + $this->blocks = array(); + } elseif ($node instanceof Twig_Node_Expression_Filter) { + return $this->preEscapeFilterNode($node, $env); + } elseif ($node instanceof Twig_Node_Print) { + return $this->escapePrintNode($node, $env, $this->needEscaping($env)); + } + + if ($node instanceof Twig_Node_AutoEscape || $node instanceof Twig_Node_Block) { + array_pop($this->statusStack); + } elseif ($node instanceof Twig_Node_BlockReference) { + $this->blocks[$node->getAttribute('name')] = $this->needEscaping($env); + } + + return $node; + } + + protected function escapePrintNode(Twig_Node_Print $node, Twig_Environment $env, $type) + { + if (false === $type) { + return $node; + } + + $expression = $node->getNode('expr'); + + if ($this->isSafeFor($type, $expression, $env)) { + return $node; + } + + $class = get_class($node); + + return new $class( + $this->getEscaperFilter($type, $expression), + $node->getLine() + ); + } + + protected function preEscapeFilterNode(Twig_Node_Expression_Filter $filter, Twig_Environment $env) + { + $name = $filter->getNode('filter')->getAttribute('value'); + + $type = $env->getFilter($name)->getPreEscape(); + if (null === $type) { + return $filter; + } + + $node = $filter->getNode('node'); + if ($this->isSafeFor($type, $node, $env)) { + return $filter; + } + + $filter->setNode('node', $this->getEscaperFilter($type, $node)); + + return $filter; + } + + protected function isSafeFor($type, Twig_NodeInterface $expression, $env) + { + $safe = $this->safeAnalysis->getSafe($expression); + + if (null === $safe) { + if (null === $this->traverser) { + $this->traverser = new Twig_NodeTraverser($env, array($this->safeAnalysis)); + } + + $this->safeAnalysis->setSafeVars($this->safeVars); + + $this->traverser->traverse($expression); + $safe = $this->safeAnalysis->getSafe($expression); + } + + return in_array($type, $safe) || in_array('all', $safe); + } + + protected function needEscaping(Twig_Environment $env) + { + if (count($this->statusStack)) { + return $this->statusStack[count($this->statusStack) - 1]; + } + + return $this->defaultStrategy ? $this->defaultStrategy : false; + } + + protected function getEscaperFilter($type, Twig_NodeInterface $node) + { + $line = $node->getLine(); + $name = new Twig_Node_Expression_Constant('escape', $line); + $args = new Twig_Node(array(new Twig_Node_Expression_Constant((string) $type, $line), new Twig_Node_Expression_Constant(null, $line), new Twig_Node_Expression_Constant(true, $line))); + + return new Twig_Node_Expression_Filter($node, $name, $args, $line); + } + + /** + * {@inheritdoc} + */ + public function getPriority() + { + return 0; + } +} diff --git a/public/system/library/template/Twig/NodeVisitor/Optimizer.php b/public/system/library/template/Twig/NodeVisitor/Optimizer.php new file mode 100644 index 0000000..872b7fe --- /dev/null +++ b/public/system/library/template/Twig/NodeVisitor/Optimizer.php @@ -0,0 +1,271 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Twig_NodeVisitor_Optimizer tries to optimizes the AST. + * + * This visitor is always the last registered one. + * + * You can configure which optimizations you want to activate via the + * optimizer mode. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_NodeVisitor_Optimizer extends Twig_BaseNodeVisitor +{ + const OPTIMIZE_ALL = -1; + const OPTIMIZE_NONE = 0; + const OPTIMIZE_FOR = 2; + const OPTIMIZE_RAW_FILTER = 4; + const OPTIMIZE_VAR_ACCESS = 8; + + protected $loops = array(); + protected $loopsTargets = array(); + protected $optimizers; + protected $prependedNodes = array(); + protected $inABody = false; + + /** + * Constructor. + * + * @param int $optimizers The optimizer mode + */ + public function __construct($optimizers = -1) + { + if (!is_int($optimizers) || $optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_VAR_ACCESS)) { + throw new InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers)); + } + + $this->optimizers = $optimizers; + } + + /** + * {@inheritdoc} + */ + protected function doEnterNode(Twig_Node $node, Twig_Environment $env) + { + if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { + $this->enterOptimizeFor($node, $env); + } + + if (PHP_VERSION_ID < 50400 && self::OPTIMIZE_VAR_ACCESS === (self::OPTIMIZE_VAR_ACCESS & $this->optimizers) && !$env->isStrictVariables() && !$env->hasExtension('sandbox')) { + if ($this->inABody) { + if (!$node instanceof Twig_Node_Expression) { + if (get_class($node) !== 'Twig_Node') { + array_unshift($this->prependedNodes, array()); + } + } else { + $node = $this->optimizeVariables($node, $env); + } + } elseif ($node instanceof Twig_Node_Body) { + $this->inABody = true; + } + } + + return $node; + } + + /** + * {@inheritdoc} + */ + protected function doLeaveNode(Twig_Node $node, Twig_Environment $env) + { + $expression = $node instanceof Twig_Node_Expression; + + if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { + $this->leaveOptimizeFor($node, $env); + } + + if (self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $this->optimizers)) { + $node = $this->optimizeRawFilter($node, $env); + } + + $node = $this->optimizePrintNode($node, $env); + + if (self::OPTIMIZE_VAR_ACCESS === (self::OPTIMIZE_VAR_ACCESS & $this->optimizers) && !$env->isStrictVariables() && !$env->hasExtension('sandbox')) { + if ($node instanceof Twig_Node_Body) { + $this->inABody = false; + } elseif ($this->inABody) { + if (!$expression && get_class($node) !== 'Twig_Node' && $prependedNodes = array_shift($this->prependedNodes)) { + $nodes = array(); + foreach (array_unique($prependedNodes) as $name) { + $nodes[] = new Twig_Node_SetTemp($name, $node->getLine()); + } + + $nodes[] = $node; + $node = new Twig_Node($nodes); + } + } + } + + return $node; + } + + protected function optimizeVariables(Twig_NodeInterface $node, Twig_Environment $env) + { + if ('Twig_Node_Expression_Name' === get_class($node) && $node->isSimple()) { + $this->prependedNodes[0][] = $node->getAttribute('name'); + + return new Twig_Node_Expression_TempName($node->getAttribute('name'), $node->getLine()); + } + + return $node; + } + + /** + * Optimizes print nodes. + * + * It replaces: + * + * * "echo $this->render(Parent)Block()" with "$this->display(Parent)Block()" + * + * @param Twig_NodeInterface $node A Node + * @param Twig_Environment $env The current Twig environment + * + * @return Twig_NodeInterface + */ + protected function optimizePrintNode(Twig_NodeInterface $node, Twig_Environment $env) + { + if (!$node instanceof Twig_Node_Print) { + return $node; + } + + if ( + $node->getNode('expr') instanceof Twig_Node_Expression_BlockReference || + $node->getNode('expr') instanceof Twig_Node_Expression_Parent + ) { + $node->getNode('expr')->setAttribute('output', true); + + return $node->getNode('expr'); + } + + return $node; + } + + /** + * Removes "raw" filters. + * + * @param Twig_NodeInterface $node A Node + * @param Twig_Environment $env The current Twig environment + * + * @return Twig_NodeInterface + */ + protected function optimizeRawFilter(Twig_NodeInterface $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_Expression_Filter && 'raw' == $node->getNode('filter')->getAttribute('value')) { + return $node->getNode('node'); + } + + return $node; + } + + /** + * Optimizes "for" tag by removing the "loop" variable creation whenever possible. + * + * @param Twig_NodeInterface $node A Node + * @param Twig_Environment $env The current Twig environment + */ + protected function enterOptimizeFor(Twig_NodeInterface $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_For) { + // disable the loop variable by default + $node->setAttribute('with_loop', false); + array_unshift($this->loops, $node); + array_unshift($this->loopsTargets, $node->getNode('value_target')->getAttribute('name')); + array_unshift($this->loopsTargets, $node->getNode('key_target')->getAttribute('name')); + } elseif (!$this->loops) { + // we are outside a loop + return; + } + + // when do we need to add the loop variable back? + + // the loop variable is referenced for the current loop + elseif ($node instanceof Twig_Node_Expression_Name && 'loop' === $node->getAttribute('name')) { + $node->setAttribute('always_defined', true); + $this->addLoopToCurrent(); + } + + // optimize access to loop targets + elseif ($node instanceof Twig_Node_Expression_Name && in_array($node->getAttribute('name'), $this->loopsTargets)) { + $node->setAttribute('always_defined', true); + } + + // block reference + elseif ($node instanceof Twig_Node_BlockReference || $node instanceof Twig_Node_Expression_BlockReference) { + $this->addLoopToCurrent(); + } + + // include without the only attribute + elseif ($node instanceof Twig_Node_Include && !$node->getAttribute('only')) { + $this->addLoopToAll(); + } + + // include function without the with_context=false parameter + elseif ($node instanceof Twig_Node_Expression_Function + && 'include' === $node->getAttribute('name') + && (!$node->getNode('arguments')->hasNode('with_context') + || false !== $node->getNode('arguments')->getNode('with_context')->getAttribute('value') + ) + ) { + $this->addLoopToAll(); + } + + // the loop variable is referenced via an attribute + elseif ($node instanceof Twig_Node_Expression_GetAttr + && (!$node->getNode('attribute') instanceof Twig_Node_Expression_Constant + || 'parent' === $node->getNode('attribute')->getAttribute('value') + ) + && (true === $this->loops[0]->getAttribute('with_loop') + || ($node->getNode('node') instanceof Twig_Node_Expression_Name + && 'loop' === $node->getNode('node')->getAttribute('name') + ) + ) + ) { + $this->addLoopToAll(); + } + } + + /** + * Optimizes "for" tag by removing the "loop" variable creation whenever possible. + * + * @param Twig_NodeInterface $node A Node + * @param Twig_Environment $env The current Twig environment + */ + protected function leaveOptimizeFor(Twig_NodeInterface $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_For) { + array_shift($this->loops); + array_shift($this->loopsTargets); + array_shift($this->loopsTargets); + } + } + + protected function addLoopToCurrent() + { + $this->loops[0]->setAttribute('with_loop', true); + } + + protected function addLoopToAll() + { + foreach ($this->loops as $loop) { + $loop->setAttribute('with_loop', true); + } + } + + /** + * {@inheritdoc} + */ + public function getPriority() + { + return 255; + } +} diff --git a/public/system/library/template/Twig/NodeVisitor/SafeAnalysis.php b/public/system/library/template/Twig/NodeVisitor/SafeAnalysis.php new file mode 100644 index 0000000..439f5bf --- /dev/null +++ b/public/system/library/template/Twig/NodeVisitor/SafeAnalysis.php @@ -0,0 +1,154 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +class Twig_NodeVisitor_SafeAnalysis extends Twig_BaseNodeVisitor +{ + protected $data = array(); + protected $safeVars = array(); + + public function setSafeVars($safeVars) + { + $this->safeVars = $safeVars; + } + + public function getSafe(Twig_NodeInterface $node) + { + $hash = spl_object_hash($node); + if (!isset($this->data[$hash])) { + return; + } + + foreach ($this->data[$hash] as $bucket) { + if ($bucket['key'] !== $node) { + continue; + } + + if (in_array('html_attr', $bucket['value'])) { + $bucket['value'][] = 'html'; + } + + return $bucket['value']; + } + } + + protected function setSafe(Twig_NodeInterface $node, array $safe) + { + $hash = spl_object_hash($node); + if (isset($this->data[$hash])) { + foreach ($this->data[$hash] as &$bucket) { + if ($bucket['key'] === $node) { + $bucket['value'] = $safe; + + return; + } + } + } + $this->data[$hash][] = array( + 'key' => $node, + 'value' => $safe, + ); + } + + /** + * {@inheritdoc} + */ + protected function doEnterNode(Twig_Node $node, Twig_Environment $env) + { + return $node; + } + + /** + * {@inheritdoc} + */ + protected function doLeaveNode(Twig_Node $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_Expression_Constant) { + // constants are marked safe for all + $this->setSafe($node, array('all')); + } elseif ($node instanceof Twig_Node_Expression_BlockReference) { + // blocks are safe by definition + $this->setSafe($node, array('all')); + } elseif ($node instanceof Twig_Node_Expression_Parent) { + // parent block is safe by definition + $this->setSafe($node, array('all')); + } elseif ($node instanceof Twig_Node_Expression_Conditional) { + // intersect safeness of both operands + $safe = $this->intersectSafe($this->getSafe($node->getNode('expr2')), $this->getSafe($node->getNode('expr3'))); + $this->setSafe($node, $safe); + } elseif ($node instanceof Twig_Node_Expression_Filter) { + // filter expression is safe when the filter is safe + $name = $node->getNode('filter')->getAttribute('value'); + $args = $node->getNode('arguments'); + if (false !== $filter = $env->getFilter($name)) { + $safe = $filter->getSafe($args); + if (null === $safe) { + $safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety()); + } + $this->setSafe($node, $safe); + } else { + $this->setSafe($node, array()); + } + } elseif ($node instanceof Twig_Node_Expression_Function) { + // function expression is safe when the function is safe + $name = $node->getAttribute('name'); + $args = $node->getNode('arguments'); + $function = $env->getFunction($name); + if (false !== $function) { + $this->setSafe($node, $function->getSafe($args)); + } else { + $this->setSafe($node, array()); + } + } elseif ($node instanceof Twig_Node_Expression_MethodCall) { + if ($node->getAttribute('safe')) { + $this->setSafe($node, array('all')); + } else { + $this->setSafe($node, array()); + } + } elseif ($node instanceof Twig_Node_Expression_GetAttr && $node->getNode('node') instanceof Twig_Node_Expression_Name) { + $name = $node->getNode('node')->getAttribute('name'); + // attributes on template instances are safe + if ('_self' == $name || in_array($name, $this->safeVars)) { + $this->setSafe($node, array('all')); + } else { + $this->setSafe($node, array()); + } + } else { + $this->setSafe($node, array()); + } + + return $node; + } + + protected function intersectSafe(array $a = null, array $b = null) + { + if (null === $a || null === $b) { + return array(); + } + + if (in_array('all', $a)) { + return $b; + } + + if (in_array('all', $b)) { + return $a; + } + + return array_intersect($a, $b); + } + + /** + * {@inheritdoc} + */ + public function getPriority() + { + return 0; + } +} diff --git a/public/system/library/template/Twig/NodeVisitor/Sandbox.php b/public/system/library/template/Twig/NodeVisitor/Sandbox.php new file mode 100644 index 0000000..7f1b913 --- /dev/null +++ b/public/system/library/template/Twig/NodeVisitor/Sandbox.php @@ -0,0 +1,82 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Twig_NodeVisitor_Sandbox implements sandboxing. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_NodeVisitor_Sandbox extends Twig_BaseNodeVisitor +{ + protected $inAModule = false; + protected $tags; + protected $filters; + protected $functions; + + /** + * {@inheritdoc} + */ + protected function doEnterNode(Twig_Node $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_Module) { + $this->inAModule = true; + $this->tags = array(); + $this->filters = array(); + $this->functions = array(); + + return $node; + } elseif ($this->inAModule) { + // look for tags + if ($node->getNodeTag() && !isset($this->tags[$node->getNodeTag()])) { + $this->tags[$node->getNodeTag()] = $node; + } + + // look for filters + if ($node instanceof Twig_Node_Expression_Filter && !isset($this->filters[$node->getNode('filter')->getAttribute('value')])) { + $this->filters[$node->getNode('filter')->getAttribute('value')] = $node; + } + + // look for functions + if ($node instanceof Twig_Node_Expression_Function && !isset($this->functions[$node->getAttribute('name')])) { + $this->functions[$node->getAttribute('name')] = $node; + } + + // wrap print to check __toString() calls + if ($node instanceof Twig_Node_Print) { + return new Twig_Node_SandboxedPrint($node->getNode('expr'), $node->getLine(), $node->getNodeTag()); + } + } + + return $node; + } + + /** + * {@inheritdoc} + */ + protected function doLeaveNode(Twig_Node $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_Module) { + $this->inAModule = false; + + $node->setNode('display_start', new Twig_Node(array(new Twig_Node_CheckSecurity($this->filters, $this->tags, $this->functions), $node->getNode('display_start')))); + } + + return $node; + } + + /** + * {@inheritdoc} + */ + public function getPriority() + { + return 0; + } +} diff --git a/public/system/library/template/Twig/NodeVisitorInterface.php b/public/system/library/template/Twig/NodeVisitorInterface.php new file mode 100644 index 0000000..f276163 --- /dev/null +++ b/public/system/library/template/Twig/NodeVisitorInterface.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Twig_NodeVisitorInterface is the interface the all node visitor classes must implement. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +interface Twig_NodeVisitorInterface +{ + /** + * Called before child nodes are visited. + * + * @param Twig_NodeInterface $node The node to visit + * @param Twig_Environment $env The Twig environment instance + * + * @return Twig_NodeInterface The modified node + */ + public function enterNode(Twig_NodeInterface $node, Twig_Environment $env); + + /** + * Called after child nodes are visited. + * + * @param Twig_NodeInterface $node The node to visit + * @param Twig_Environment $env The Twig environment instance + * + * @return Twig_NodeInterface|false The modified node or false if the node must be removed + */ + public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env); + + /** + * Returns the priority for this visitor. + * + * Priority should be between -10 and 10 (0 is the default). + * + * @return int The priority level + */ + public function getPriority(); +} diff --git a/public/system/library/template/Twig/Parser.php b/public/system/library/template/Twig/Parser.php new file mode 100644 index 0000000..4628d67 --- /dev/null +++ b/public/system/library/template/Twig/Parser.php @@ -0,0 +1,399 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Default parser implementation. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Parser implements Twig_ParserInterface +{ + protected $stack = array(); + protected $stream; + protected $parent; + protected $handlers; + protected $visitors; + protected $expressionParser; + protected $blocks; + protected $blockStack; + protected $macros; + protected $env; + protected $reservedMacroNames; + protected $importedSymbols; + protected $traits; + protected $embeddedTemplates = array(); + + /** + * Constructor. + * + * @param Twig_Environment $env A Twig_Environment instance + */ + public function __construct(Twig_Environment $env) + { + $this->env = $env; + } + + public function getEnvironment() + { + return $this->env; + } + + public function getVarName() + { + return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false)); + } + + public function getFilename() + { + return $this->stream->getFilename(); + } + + /** + * {@inheritdoc} + */ + public function parse(Twig_TokenStream $stream, $test = null, $dropNeedle = false) + { + // push all variables into the stack to keep the current state of the parser + // using get_object_vars() instead of foreach would lead to https://bugs.php.net/71336 + $vars = array(); + foreach ($this as $k => $v) { + $vars[$k] = $v; + } + + unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames']); + $this->stack[] = $vars; + + // tag handlers + if (null === $this->handlers) { + $this->handlers = $this->env->getTokenParsers(); + $this->handlers->setParser($this); + } + + // node visitors + if (null === $this->visitors) { + $this->visitors = $this->env->getNodeVisitors(); + } + + if (null === $this->expressionParser) { + $this->expressionParser = new Twig_ExpressionParser($this, $this->env->getUnaryOperators(), $this->env->getBinaryOperators()); + } + + $this->stream = $stream; + $this->parent = null; + $this->blocks = array(); + $this->macros = array(); + $this->traits = array(); + $this->blockStack = array(); + $this->importedSymbols = array(array()); + $this->embeddedTemplates = array(); + + try { + $body = $this->subparse($test, $dropNeedle); + + if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) { + $body = new Twig_Node(); + } + } catch (Twig_Error_Syntax $e) { + if (!$e->getTemplateFile()) { + $e->setTemplateFile($this->getFilename()); + } + + if (!$e->getTemplateLine()) { + $e->setTemplateLine($this->stream->getCurrent()->getLine()); + } + + throw $e; + } + + $node = new Twig_Node_Module(new Twig_Node_Body(array($body)), $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), $this->embeddedTemplates, $this->getFilename()); + + $traverser = new Twig_NodeTraverser($this->env, $this->visitors); + + $node = $traverser->traverse($node); + + // restore previous stack so previous parse() call can resume working + foreach (array_pop($this->stack) as $key => $val) { + $this->$key = $val; + } + + return $node; + } + + public function subparse($test, $dropNeedle = false) + { + $lineno = $this->getCurrentToken()->getLine(); + $rv = array(); + while (!$this->stream->isEOF()) { + switch ($this->getCurrentToken()->getType()) { + case Twig_Token::TEXT_TYPE: + $token = $this->stream->next(); + $rv[] = new Twig_Node_Text($token->getValue(), $token->getLine()); + break; + + case Twig_Token::VAR_START_TYPE: + $token = $this->stream->next(); + $expr = $this->expressionParser->parseExpression(); + $this->stream->expect(Twig_Token::VAR_END_TYPE); + $rv[] = new Twig_Node_Print($expr, $token->getLine()); + break; + + case Twig_Token::BLOCK_START_TYPE: + $this->stream->next(); + $token = $this->getCurrentToken(); + + if ($token->getType() !== Twig_Token::NAME_TYPE) { + throw new Twig_Error_Syntax('A block must start with a tag name.', $token->getLine(), $this->getFilename()); + } + + if (null !== $test && call_user_func($test, $token)) { + if ($dropNeedle) { + $this->stream->next(); + } + + if (1 === count($rv)) { + return $rv[0]; + } + + return new Twig_Node($rv, array(), $lineno); + } + + $subparser = $this->handlers->getTokenParser($token->getValue()); + if (null === $subparser) { + if (null !== $test) { + $e = new Twig_Error_Syntax(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->getFilename()); + + if (is_array($test) && isset($test[0]) && $test[0] instanceof Twig_TokenParserInterface) { + $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno)); + } + } else { + $e = new Twig_Error_Syntax(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->getFilename()); + $e->addSuggestions($token->getValue(), array_keys($this->env->getTags())); + } + + throw $e; + } + + $this->stream->next(); + + $node = $subparser->parse($token); + if (null !== $node) { + $rv[] = $node; + } + break; + + default: + throw new Twig_Error_Syntax('Lexer or parser ended up in unsupported state.', 0, $this->getFilename()); + } + } + + if (1 === count($rv)) { + return $rv[0]; + } + + return new Twig_Node($rv, array(), $lineno); + } + + public function addHandler($name, $class) + { + $this->handlers[$name] = $class; + } + + public function addNodeVisitor(Twig_NodeVisitorInterface $visitor) + { + $this->visitors[] = $visitor; + } + + public function getBlockStack() + { + return $this->blockStack; + } + + public function peekBlockStack() + { + return $this->blockStack[count($this->blockStack) - 1]; + } + + public function popBlockStack() + { + array_pop($this->blockStack); + } + + public function pushBlockStack($name) + { + $this->blockStack[] = $name; + } + + public function hasBlock($name) + { + return isset($this->blocks[$name]); + } + + public function getBlock($name) + { + return $this->blocks[$name]; + } + + public function setBlock($name, Twig_Node_Block $value) + { + $this->blocks[$name] = new Twig_Node_Body(array($value), array(), $value->getLine()); + } + + public function hasMacro($name) + { + return isset($this->macros[$name]); + } + + public function setMacro($name, Twig_Node_Macro $node) + { + if ($this->isReservedMacroName($name)) { + throw new Twig_Error_Syntax(sprintf('"%s" cannot be used as a macro name as it is a reserved keyword.', $name), $node->getLine(), $this->getFilename()); + } + + $this->macros[$name] = $node; + } + + public function isReservedMacroName($name) + { + if (null === $this->reservedMacroNames) { + $this->reservedMacroNames = array(); + $r = new ReflectionClass($this->env->getBaseTemplateClass()); + foreach ($r->getMethods() as $method) { + $methodName = strtolower($method->getName()); + + if ('get' === substr($methodName, 0, 3) && isset($methodName[3])) { + $this->reservedMacroNames[] = substr($methodName, 3); + } + } + } + + return in_array(strtolower($name), $this->reservedMacroNames); + } + + public function addTrait($trait) + { + $this->traits[] = $trait; + } + + public function hasTraits() + { + return count($this->traits) > 0; + } + + public function embedTemplate(Twig_Node_Module $template) + { + $template->setIndex(mt_rand()); + + $this->embeddedTemplates[] = $template; + } + + public function addImportedSymbol($type, $alias, $name = null, Twig_Node_Expression $node = null) + { + $this->importedSymbols[0][$type][$alias] = array('name' => $name, 'node' => $node); + } + + public function getImportedSymbol($type, $alias) + { + foreach ($this->importedSymbols as $functions) { + if (isset($functions[$type][$alias])) { + return $functions[$type][$alias]; + } + } + } + + public function isMainScope() + { + return 1 === count($this->importedSymbols); + } + + public function pushLocalScope() + { + array_unshift($this->importedSymbols, array()); + } + + public function popLocalScope() + { + array_shift($this->importedSymbols); + } + + /** + * Gets the expression parser. + * + * @return Twig_ExpressionParser The expression parser + */ + public function getExpressionParser() + { + return $this->expressionParser; + } + + public function getParent() + { + return $this->parent; + } + + public function setParent($parent) + { + $this->parent = $parent; + } + + /** + * Gets the token stream. + * + * @return Twig_TokenStream The token stream + */ + public function getStream() + { + return $this->stream; + } + + /** + * Gets the current token. + * + * @return Twig_Token The current token + */ + public function getCurrentToken() + { + return $this->stream->getCurrent(); + } + + protected function filterBodyNodes(Twig_NodeInterface $node) + { + // check that the body does not contain non-empty output nodes + if ( + ($node instanceof Twig_Node_Text && !ctype_space($node->getAttribute('data'))) + || + (!$node instanceof Twig_Node_Text && !$node instanceof Twig_Node_BlockReference && $node instanceof Twig_NodeOutputInterface) + ) { + if (false !== strpos((string) $node, chr(0xEF).chr(0xBB).chr(0xBF))) { + throw new Twig_Error_Syntax('A template that extends another one cannot have a body but a byte order mark (BOM) has been detected; it must be removed.', $node->getLine(), $this->getFilename()); + } + + throw new Twig_Error_Syntax('A template that extends another one cannot have a body.', $node->getLine(), $this->getFilename()); + } + + // bypass "set" nodes as they "capture" the output + if ($node instanceof Twig_Node_Set) { + return $node; + } + + if ($node instanceof Twig_NodeOutputInterface) { + return; + } + + foreach ($node as $k => $n) { + if (null !== $n && null === $this->filterBodyNodes($n)) { + $node->removeNode($k); + } + } + + return $node; + } +} diff --git a/public/system/library/template/Twig/ParserInterface.php b/public/system/library/template/Twig/ParserInterface.php new file mode 100644 index 0000000..8e7cc0a --- /dev/null +++ b/public/system/library/template/Twig/ParserInterface.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface implemented by parser classes. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 3.0) + */ +interface Twig_ParserInterface +{ + /** + * Converts a token stream to a node tree. + * + * @param Twig_TokenStream $stream A token stream instance + * + * @return Twig_Node_Module A node tree + * + * @throws Twig_Error_Syntax When the token stream is syntactically or semantically wrong + */ + public function parse(Twig_TokenStream $stream); +} diff --git a/public/system/library/template/Twig/Profiler/Dumper/Blackfire.php b/public/system/library/template/Twig/Profiler/Dumper/Blackfire.php new file mode 100644 index 0000000..b82747a --- /dev/null +++ b/public/system/library/template/Twig/Profiler/Dumper/Blackfire.php @@ -0,0 +1,68 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Profiler_Dumper_Blackfire +{ + public function dump(Twig_Profiler_Profile $profile) + { + $data = array(); + $this->dumpProfile('main()', $profile, $data); + $this->dumpChildren('main()', $profile, $data); + + $start = microtime(true); + $str = <<<EOF +file-format: BlackfireProbe +cost-dimensions: wt mu pmu +request-start: {$start} + + +EOF; + + foreach ($data as $name => $values) { + $str .= "{$name}//{$values['ct']} {$values['wt']} {$values['mu']} {$values['pmu']}\n"; + } + + return $str; + } + + private function dumpChildren($parent, Twig_Profiler_Profile $profile, &$data) + { + foreach ($profile as $p) { + if ($p->isTemplate()) { + $name = $p->getTemplate(); + } else { + $name = sprintf('%s::%s(%s)', $p->getTemplate(), $p->getType(), $p->getName()); + } + $this->dumpProfile(sprintf('%s==>%s', $parent, $name), $p, $data); + $this->dumpChildren($name, $p, $data); + } + } + + private function dumpProfile($edge, Twig_Profiler_Profile $profile, &$data) + { + if (isset($data[$edge])) { + $data[$edge]['ct'] += 1; + $data[$edge]['wt'] += floor($profile->getDuration() * 1000000); + $data[$edge]['mu'] += $profile->getMemoryUsage(); + $data[$edge]['pmu'] += $profile->getPeakMemoryUsage(); + } else { + $data[$edge] = array( + 'ct' => 1, + 'wt' => floor($profile->getDuration() * 1000000), + 'mu' => $profile->getMemoryUsage(), + 'pmu' => $profile->getPeakMemoryUsage(), + ); + } + } +} diff --git a/public/system/library/template/Twig/Profiler/Dumper/Html.php b/public/system/library/template/Twig/Profiler/Dumper/Html.php new file mode 100644 index 0000000..f066da7 --- /dev/null +++ b/public/system/library/template/Twig/Profiler/Dumper/Html.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Profiler_Dumper_Html extends Twig_Profiler_Dumper_Text +{ + private static $colors = array( + 'block' => '#dfd', + 'macro' => '#ddf', + 'template' => '#ffd', + 'big' => '#d44', + ); + + public function dump(Twig_Profiler_Profile $profile) + { + return '<pre>'.parent::dump($profile).'</pre>'; + } + + protected function formatTemplate(Twig_Profiler_Profile $profile, $prefix) + { + return sprintf('%s└ <span style="background-color: %s">%s</span>', $prefix, self::$colors['template'], $profile->getTemplate()); + } + + protected function formatNonTemplate(Twig_Profiler_Profile $profile, $prefix) + { + return sprintf('%s└ %s::%s(<span style="background-color: %s">%s</span>)', $prefix, $profile->getTemplate(), $profile->getType(), isset(self::$colors[$profile->getType()]) ? self::$colors[$profile->getType()] : 'auto', $profile->getName()); + } + + protected function formatTime(Twig_Profiler_Profile $profile, $percent) + { + return sprintf('<span style="color: %s">%.2fms/%.0f%%</span>', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent); + } +} diff --git a/public/system/library/template/Twig/Profiler/Dumper/Text.php b/public/system/library/template/Twig/Profiler/Dumper/Text.php new file mode 100644 index 0000000..998e210 --- /dev/null +++ b/public/system/library/template/Twig/Profiler/Dumper/Text.php @@ -0,0 +1,68 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Profiler_Dumper_Text +{ + private $root; + + public function dump(Twig_Profiler_Profile $profile) + { + return $this->dumpProfile($profile); + } + + protected function formatTemplate(Twig_Profiler_Profile $profile, $prefix) + { + return sprintf('%s└ %s', $prefix, $profile->getTemplate()); + } + + protected function formatNonTemplate(Twig_Profiler_Profile $profile, $prefix) + { + return sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName()); + } + + protected function formatTime(Twig_Profiler_Profile $profile, $percent) + { + return sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent); + } + + private function dumpProfile(Twig_Profiler_Profile $profile, $prefix = '', $sibling = false) + { + if ($profile->isRoot()) { + $this->root = $profile->getDuration(); + $start = $profile->getName(); + } else { + if ($profile->isTemplate()) { + $start = $this->formatTemplate($profile, $prefix); + } else { + $start = $this->formatNonTemplate($profile, $prefix); + } + $prefix .= $sibling ? '│ ' : ' '; + } + + $percent = $this->root ? $profile->getDuration() / $this->root * 100 : 0; + + if ($profile->getDuration() * 1000 < 1) { + $str = $start."\n"; + } else { + $str = sprintf("%s %s\n", $start, $this->formatTime($profile, $percent)); + } + + $nCount = count($profile->getProfiles()); + foreach ($profile as $i => $p) { + $str .= $this->dumpProfile($p, $prefix, $i + 1 !== $nCount); + } + + return $str; + } +} diff --git a/public/system/library/template/Twig/Profiler/Node/EnterProfile.php b/public/system/library/template/Twig/Profiler/Node/EnterProfile.php new file mode 100644 index 0000000..2f97214 --- /dev/null +++ b/public/system/library/template/Twig/Profiler/Node/EnterProfile.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a profile enter node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Profiler_Node_EnterProfile extends Twig_Node +{ + public function __construct($extensionName, $type, $name, $varName) + { + parent::__construct(array(), array('extension_name' => $extensionName, 'name' => $name, 'type' => $type, 'var_name' => $varName)); + } + + /** + * {@inheritdoc} + */ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->write(sprintf('$%s = $this->env->getExtension(', $this->getAttribute('var_name'))) + ->repr($this->getAttribute('extension_name')) + ->raw(");\n") + ->write(sprintf('$%s->enter($%s = new Twig_Profiler_Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) + ->repr($this->getAttribute('type')) + ->raw(', ') + ->repr($this->getAttribute('name')) + ->raw("));\n\n") + ; + } +} diff --git a/public/system/library/template/Twig/Profiler/Node/LeaveProfile.php b/public/system/library/template/Twig/Profiler/Node/LeaveProfile.php new file mode 100644 index 0000000..88074c2 --- /dev/null +++ b/public/system/library/template/Twig/Profiler/Node/LeaveProfile.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a profile leave node. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Profiler_Node_LeaveProfile extends Twig_Node +{ + public function __construct($varName) + { + parent::__construct(array(), array('var_name' => $varName)); + } + + /** + * {@inheritdoc} + */ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->write("\n") + ->write(sprintf("\$%s->leave(\$%s);\n\n", $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) + ; + } +} diff --git a/public/system/library/template/Twig/Profiler/NodeVisitor/Profiler.php b/public/system/library/template/Twig/Profiler/NodeVisitor/Profiler.php new file mode 100644 index 0000000..4b0baa8 --- /dev/null +++ b/public/system/library/template/Twig/Profiler/NodeVisitor/Profiler.php @@ -0,0 +1,72 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Profiler_NodeVisitor_Profiler extends Twig_BaseNodeVisitor +{ + private $extensionName; + + public function __construct($extensionName) + { + $this->extensionName = $extensionName; + } + + /** + * {@inheritdoc} + */ + protected function doEnterNode(Twig_Node $node, Twig_Environment $env) + { + return $node; + } + + /** + * {@inheritdoc} + */ + protected function doLeaveNode(Twig_Node $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_Module) { + $varName = $this->getVarName(); + $node->setNode('display_start', new Twig_Node(array(new Twig_Profiler_Node_EnterProfile($this->extensionName, Twig_Profiler_Profile::TEMPLATE, $node->getAttribute('filename'), $varName), $node->getNode('display_start')))); + $node->setNode('display_end', new Twig_Node(array(new Twig_Profiler_Node_LeaveProfile($varName), $node->getNode('display_end')))); + } elseif ($node instanceof Twig_Node_Block) { + $varName = $this->getVarName(); + $node->setNode('body', new Twig_Node_Body(array( + new Twig_Profiler_Node_EnterProfile($this->extensionName, Twig_Profiler_Profile::BLOCK, $node->getAttribute('name'), $varName), + $node->getNode('body'), + new Twig_Profiler_Node_LeaveProfile($varName), + ))); + } elseif ($node instanceof Twig_Node_Macro) { + $varName = $this->getVarName(); + $node->setNode('body', new Twig_Node_Body(array( + new Twig_Profiler_Node_EnterProfile($this->extensionName, Twig_Profiler_Profile::MACRO, $node->getAttribute('name'), $varName), + $node->getNode('body'), + new Twig_Profiler_Node_LeaveProfile($varName), + ))); + } + + return $node; + } + + private function getVarName() + { + return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false)); + } + + /** + * {@inheritdoc} + */ + public function getPriority() + { + return 0; + } +} diff --git a/public/system/library/template/Twig/Profiler/Profile.php b/public/system/library/template/Twig/Profiler/Profile.php new file mode 100644 index 0000000..104bc05 --- /dev/null +++ b/public/system/library/template/Twig/Profiler/Profile.php @@ -0,0 +1,160 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2015 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Profiler_Profile implements IteratorAggregate, Serializable +{ + const ROOT = 'ROOT'; + const BLOCK = 'block'; + const TEMPLATE = 'template'; + const MACRO = 'macro'; + + private $template; + private $name; + private $type; + private $starts = array(); + private $ends = array(); + private $profiles = array(); + + public function __construct($template = 'main', $type = self::ROOT, $name = 'main') + { + $this->template = $template; + $this->type = $type; + $this->name = 0 === strpos($name, '__internal_') ? 'INTERNAL' : $name; + $this->enter(); + } + + public function getTemplate() + { + return $this->template; + } + + public function getType() + { + return $this->type; + } + + public function getName() + { + return $this->name; + } + + public function isRoot() + { + return self::ROOT === $this->type; + } + + public function isTemplate() + { + return self::TEMPLATE === $this->type; + } + + public function isBlock() + { + return self::BLOCK === $this->type; + } + + public function isMacro() + { + return self::MACRO === $this->type; + } + + public function getProfiles() + { + return $this->profiles; + } + + public function addProfile(Twig_Profiler_Profile $profile) + { + $this->profiles[] = $profile; + } + + /** + * Returns the duration in microseconds. + * + * @return int + */ + public function getDuration() + { + if ($this->isRoot() && $this->profiles) { + // for the root node with children, duration is the sum of all child durations + $duration = 0; + foreach ($this->profiles as $profile) { + $duration += $profile->getDuration(); + } + + return $duration; + } + + return isset($this->ends['wt']) && isset($this->starts['wt']) ? $this->ends['wt'] - $this->starts['wt'] : 0; + } + + /** + * Returns the memory usage in bytes. + * + * @return int + */ + public function getMemoryUsage() + { + return isset($this->ends['mu']) && isset($this->starts['mu']) ? $this->ends['mu'] - $this->starts['mu'] : 0; + } + + /** + * Returns the peak memory usage in bytes. + * + * @return int + */ + public function getPeakMemoryUsage() + { + return isset($this->ends['pmu']) && isset($this->starts['pmu']) ? $this->ends['pmu'] - $this->starts['pmu'] : 0; + } + + /** + * Starts the profiling. + */ + public function enter() + { + $this->starts = array( + 'wt' => microtime(true), + 'mu' => memory_get_usage(), + 'pmu' => memory_get_peak_usage(), + ); + } + + /** + * Stops the profiling. + */ + public function leave() + { + $this->ends = array( + 'wt' => microtime(true), + 'mu' => memory_get_usage(), + 'pmu' => memory_get_peak_usage(), + ); + } + + public function getIterator() + { + return new ArrayIterator($this->profiles); + } + + public function serialize() + { + return serialize(array($this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles)); + } + + public function unserialize($data) + { + list($this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles) = unserialize($data); + } +} diff --git a/public/system/library/template/Twig/Sandbox/SecurityError.php b/public/system/library/template/Twig/Sandbox/SecurityError.php new file mode 100644 index 0000000..015bfae --- /dev/null +++ b/public/system/library/template/Twig/Sandbox/SecurityError.php @@ -0,0 +1,19 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Exception thrown when a security error occurs at runtime. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Sandbox_SecurityError extends Twig_Error +{ +} diff --git a/public/system/library/template/Twig/Sandbox/SecurityNotAllowedFilterError.php b/public/system/library/template/Twig/Sandbox/SecurityNotAllowedFilterError.php new file mode 100644 index 0000000..99faba9 --- /dev/null +++ b/public/system/library/template/Twig/Sandbox/SecurityNotAllowedFilterError.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Exception thrown when a not allowed filter is used in a template. + * + * @author Martin Hasoň <martin.hason@gmail.com> + */ +class Twig_Sandbox_SecurityNotAllowedFilterError extends Twig_Sandbox_SecurityError +{ + private $filterName; + + public function __construct($message, $functionName, $lineno = -1, $filename = null, Exception $previous = null) + { + parent::__construct($message, $lineno, $filename, $previous); + $this->filterName = $functionName; + } + + public function getFilterName() + { + return $this->filterName; + } +} diff --git a/public/system/library/template/Twig/Sandbox/SecurityNotAllowedFunctionError.php b/public/system/library/template/Twig/Sandbox/SecurityNotAllowedFunctionError.php new file mode 100644 index 0000000..05cf488 --- /dev/null +++ b/public/system/library/template/Twig/Sandbox/SecurityNotAllowedFunctionError.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Exception thrown when a not allowed function is used in a template. + * + * @author Martin Hasoň <martin.hason@gmail.com> + */ +class Twig_Sandbox_SecurityNotAllowedFunctionError extends Twig_Sandbox_SecurityError +{ + private $functionName; + + public function __construct($message, $functionName, $lineno = -1, $filename = null, Exception $previous = null) + { + parent::__construct($message, $lineno, $filename, $previous); + $this->functionName = $functionName; + } + + public function getFunctionName() + { + return $this->functionName; + } +} diff --git a/public/system/library/template/Twig/Sandbox/SecurityNotAllowedTagError.php b/public/system/library/template/Twig/Sandbox/SecurityNotAllowedTagError.php new file mode 100644 index 0000000..b3bb5e8 --- /dev/null +++ b/public/system/library/template/Twig/Sandbox/SecurityNotAllowedTagError.php @@ -0,0 +1,31 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Exception thrown when a not allowed tag is used in a template. + * + * @author Martin Hasoň <martin.hason@gmail.com> + */ +class Twig_Sandbox_SecurityNotAllowedTagError extends Twig_Sandbox_SecurityError +{ + private $tagName; + + public function __construct($message, $tagName, $lineno = -1, $filename = null, Exception $previous = null) + { + parent::__construct($message, $lineno, $filename, $previous); + $this->tagName = $tagName; + } + + public function getTagName() + { + return $this->tagName; + } +} diff --git a/public/system/library/template/Twig/Sandbox/SecurityPolicy.php b/public/system/library/template/Twig/Sandbox/SecurityPolicy.php new file mode 100644 index 0000000..c4dd03d --- /dev/null +++ b/public/system/library/template/Twig/Sandbox/SecurityPolicy.php @@ -0,0 +1,119 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a security policy which need to be enforced when sandbox mode is enabled. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Sandbox_SecurityPolicy implements Twig_Sandbox_SecurityPolicyInterface +{ + protected $allowedTags; + protected $allowedFilters; + protected $allowedMethods; + protected $allowedProperties; + protected $allowedFunctions; + + public function __construct(array $allowedTags = array(), array $allowedFilters = array(), array $allowedMethods = array(), array $allowedProperties = array(), array $allowedFunctions = array()) + { + $this->allowedTags = $allowedTags; + $this->allowedFilters = $allowedFilters; + $this->setAllowedMethods($allowedMethods); + $this->allowedProperties = $allowedProperties; + $this->allowedFunctions = $allowedFunctions; + } + + public function setAllowedTags(array $tags) + { + $this->allowedTags = $tags; + } + + public function setAllowedFilters(array $filters) + { + $this->allowedFilters = $filters; + } + + public function setAllowedMethods(array $methods) + { + $this->allowedMethods = array(); + foreach ($methods as $class => $m) { + $this->allowedMethods[$class] = array_map('strtolower', is_array($m) ? $m : array($m)); + } + } + + public function setAllowedProperties(array $properties) + { + $this->allowedProperties = $properties; + } + + public function setAllowedFunctions(array $functions) + { + $this->allowedFunctions = $functions; + } + + public function checkSecurity($tags, $filters, $functions) + { + foreach ($tags as $tag) { + if (!in_array($tag, $this->allowedTags)) { + throw new Twig_Sandbox_SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag); + } + } + + foreach ($filters as $filter) { + if (!in_array($filter, $this->allowedFilters)) { + throw new Twig_Sandbox_SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter); + } + } + + foreach ($functions as $function) { + if (!in_array($function, $this->allowedFunctions)) { + throw new Twig_Sandbox_SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function); + } + } + } + + public function checkMethodAllowed($obj, $method) + { + if ($obj instanceof Twig_TemplateInterface || $obj instanceof Twig_Markup) { + return true; + } + + $allowed = false; + $method = strtolower($method); + foreach ($this->allowedMethods as $class => $methods) { + if ($obj instanceof $class) { + $allowed = in_array($method, $methods); + + break; + } + } + + if (!$allowed) { + throw new Twig_Sandbox_SecurityError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, get_class($obj))); + } + } + + public function checkPropertyAllowed($obj, $property) + { + $allowed = false; + foreach ($this->allowedProperties as $class => $properties) { + if ($obj instanceof $class) { + $allowed = in_array($property, is_array($properties) ? $properties : array($properties)); + + break; + } + } + + if (!$allowed) { + throw new Twig_Sandbox_SecurityError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, get_class($obj))); + } + } +} diff --git a/public/system/library/template/Twig/Sandbox/SecurityPolicyInterface.php b/public/system/library/template/Twig/Sandbox/SecurityPolicyInterface.php new file mode 100644 index 0000000..6ab48e3 --- /dev/null +++ b/public/system/library/template/Twig/Sandbox/SecurityPolicyInterface.php @@ -0,0 +1,24 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interfaces that all security policy classes must implements. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +interface Twig_Sandbox_SecurityPolicyInterface +{ + public function checkSecurity($tags, $filters, $functions); + + public function checkMethodAllowed($obj, $method); + + public function checkPropertyAllowed($obj, $method); +} diff --git a/public/system/library/template/Twig/SimpleFilter.php b/public/system/library/template/Twig/SimpleFilter.php new file mode 100644 index 0000000..0c1e092 --- /dev/null +++ b/public/system/library/template/Twig/SimpleFilter.php @@ -0,0 +1,117 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009-2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a template filter. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_SimpleFilter +{ + protected $name; + protected $callable; + protected $options; + protected $arguments = array(); + + public function __construct($name, $callable, array $options = array()) + { + $this->name = $name; + $this->callable = $callable; + $this->options = array_merge(array( + 'needs_environment' => false, + 'needs_context' => false, + 'is_variadic' => false, + 'is_safe' => null, + 'is_safe_callback' => null, + 'pre_escape' => null, + 'preserves_safety' => null, + 'node_class' => 'Twig_Node_Expression_Filter', + 'deprecated' => false, + 'alternative' => null, + ), $options); + } + + public function getName() + { + return $this->name; + } + + public function getCallable() + { + return $this->callable; + } + + public function getNodeClass() + { + return $this->options['node_class']; + } + + public function setArguments($arguments) + { + $this->arguments = $arguments; + } + + public function getArguments() + { + return $this->arguments; + } + + public function needsEnvironment() + { + return $this->options['needs_environment']; + } + + public function needsContext() + { + return $this->options['needs_context']; + } + + public function getSafe(Twig_Node $filterArgs) + { + if (null !== $this->options['is_safe']) { + return $this->options['is_safe']; + } + + if (null !== $this->options['is_safe_callback']) { + return call_user_func($this->options['is_safe_callback'], $filterArgs); + } + } + + public function getPreservesSafety() + { + return $this->options['preserves_safety']; + } + + public function getPreEscape() + { + return $this->options['pre_escape']; + } + + public function isVariadic() + { + return $this->options['is_variadic']; + } + + public function isDeprecated() + { + return (bool) $this->options['deprecated']; + } + + public function getDeprecatedVersion() + { + return $this->options['deprecated']; + } + + public function getAlternative() + { + return $this->options['alternative']; + } +} diff --git a/public/system/library/template/Twig/SimpleFunction.php b/public/system/library/template/Twig/SimpleFunction.php new file mode 100644 index 0000000..63003d6 --- /dev/null +++ b/public/system/library/template/Twig/SimpleFunction.php @@ -0,0 +1,107 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010-2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a template function. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_SimpleFunction +{ + protected $name; + protected $callable; + protected $options; + protected $arguments = array(); + + public function __construct($name, $callable, array $options = array()) + { + $this->name = $name; + $this->callable = $callable; + $this->options = array_merge(array( + 'needs_environment' => false, + 'needs_context' => false, + 'is_variadic' => false, + 'is_safe' => null, + 'is_safe_callback' => null, + 'node_class' => 'Twig_Node_Expression_Function', + 'deprecated' => false, + 'alternative' => null, + ), $options); + } + + public function getName() + { + return $this->name; + } + + public function getCallable() + { + return $this->callable; + } + + public function getNodeClass() + { + return $this->options['node_class']; + } + + public function setArguments($arguments) + { + $this->arguments = $arguments; + } + + public function getArguments() + { + return $this->arguments; + } + + public function needsEnvironment() + { + return $this->options['needs_environment']; + } + + public function needsContext() + { + return $this->options['needs_context']; + } + + public function getSafe(Twig_Node $functionArgs) + { + if (null !== $this->options['is_safe']) { + return $this->options['is_safe']; + } + + if (null !== $this->options['is_safe_callback']) { + return call_user_func($this->options['is_safe_callback'], $functionArgs); + } + + return array(); + } + + public function isVariadic() + { + return $this->options['is_variadic']; + } + + public function isDeprecated() + { + return (bool) $this->options['deprecated']; + } + + public function getDeprecatedVersion() + { + return $this->options['deprecated']; + } + + public function getAlternative() + { + return $this->options['alternative']; + } +} diff --git a/public/system/library/template/Twig/SimpleTest.php b/public/system/library/template/Twig/SimpleTest.php new file mode 100644 index 0000000..cff3ae7 --- /dev/null +++ b/public/system/library/template/Twig/SimpleTest.php @@ -0,0 +1,69 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010-2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a template test. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_SimpleTest +{ + protected $name; + protected $callable; + protected $options; + + public function __construct($name, $callable, array $options = array()) + { + $this->name = $name; + $this->callable = $callable; + $this->options = array_merge(array( + 'is_variadic' => false, + 'node_class' => 'Twig_Node_Expression_Test', + 'deprecated' => false, + 'alternative' => null, + ), $options); + } + + public function getName() + { + return $this->name; + } + + public function getCallable() + { + return $this->callable; + } + + public function getNodeClass() + { + return $this->options['node_class']; + } + + public function isVariadic() + { + return $this->options['is_variadic']; + } + + public function isDeprecated() + { + return (bool) $this->options['deprecated']; + } + + public function getDeprecatedVersion() + { + return $this->options['deprecated']; + } + + public function getAlternative() + { + return $this->options['alternative']; + } +} diff --git a/public/system/library/template/Twig/Template.php b/public/system/library/template/Twig/Template.php new file mode 100644 index 0000000..7f7f62a --- /dev/null +++ b/public/system/library/template/Twig/Template.php @@ -0,0 +1,620 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Default base class for compiled templates. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +abstract class Twig_Template implements Twig_TemplateInterface +{ + protected static $cache = array(); + + protected $parent; + protected $parents = array(); + protected $env; + protected $blocks = array(); + protected $traits = array(); + + /** + * Constructor. + * + * @param Twig_Environment $env A Twig_Environment instance + */ + public function __construct(Twig_Environment $env) + { + $this->env = $env; + } + + /** + * Returns the template name. + * + * @return string The template name + */ + abstract public function getTemplateName(); + + /** + * @deprecated since 1.20 (to be removed in 2.0) + */ + public function getEnvironment() + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 1.20 and will be removed in 2.0.', E_USER_DEPRECATED); + + return $this->env; + } + + /** + * Returns the parent template. + * + * This method is for internal use only and should never be called + * directly. + * + * @param array $context + * + * @return Twig_TemplateInterface|false The parent template or false if there is no parent + * + * @internal + */ + public function getParent(array $context) + { + if (null !== $this->parent) { + return $this->parent; + } + + try { + $parent = $this->doGetParent($context); + + if (false === $parent) { + return false; + } + + if ($parent instanceof self) { + return $this->parents[$parent->getTemplateName()] = $parent; + } + + if (!isset($this->parents[$parent])) { + $this->parents[$parent] = $this->loadTemplate($parent); + } + } catch (Twig_Error_Loader $e) { + $e->setTemplateFile(null); + $e->guess(); + + throw $e; + } + + return $this->parents[$parent]; + } + + protected function doGetParent(array $context) + { + return false; + } + + public function isTraitable() + { + return true; + } + + /** + * Displays a parent block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to display from the parent + * @param array $context The context + * @param array $blocks The current set of blocks + * + * @internal + */ + public function displayParentBlock($name, array $context, array $blocks = array()) + { + $name = (string) $name; + + if (isset($this->traits[$name])) { + $this->traits[$name][0]->displayBlock($name, $context, $blocks, false); + } elseif (false !== $parent = $this->getParent($context)) { + $parent->displayBlock($name, $context, $blocks, false); + } else { + throw new Twig_Error_Runtime(sprintf('The template has no parent and no traits defining the "%s" block', $name), -1, $this->getTemplateName()); + } + } + + /** + * Displays a block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to display + * @param array $context The context + * @param array $blocks The current set of blocks + * @param bool $useBlocks Whether to use the current set of blocks + * + * @internal + */ + public function displayBlock($name, array $context, array $blocks = array(), $useBlocks = true) + { + $name = (string) $name; + + if ($useBlocks && isset($blocks[$name])) { + $template = $blocks[$name][0]; + $block = $blocks[$name][1]; + } elseif (isset($this->blocks[$name])) { + $template = $this->blocks[$name][0]; + $block = $this->blocks[$name][1]; + } else { + $template = null; + $block = null; + } + + if (null !== $template) { + // avoid RCEs when sandbox is enabled + if (!$template instanceof self) { + throw new LogicException('A block must be a method on a Twig_Template instance.'); + } + + try { + $template->$block($context, $blocks); + } catch (Twig_Error $e) { + if (!$e->getTemplateFile()) { + $e->setTemplateFile($template->getTemplateName()); + } + + // this is mostly useful for Twig_Error_Loader exceptions + // see Twig_Error_Loader + if (false === $e->getTemplateLine()) { + $e->setTemplateLine(-1); + $e->guess(); + } + + throw $e; + } catch (Exception $e) { + throw new Twig_Error_Runtime(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getTemplateName(), $e); + } + } elseif (false !== $parent = $this->getParent($context)) { + $parent->displayBlock($name, $context, array_merge($this->blocks, $blocks), false); + } + } + + /** + * Renders a parent block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to render from the parent + * @param array $context The context + * @param array $blocks The current set of blocks + * + * @return string The rendered block + * + * @internal + */ + public function renderParentBlock($name, array $context, array $blocks = array()) + { + ob_start(); + $this->displayParentBlock($name, $context, $blocks); + + return ob_get_clean(); + } + + /** + * Renders a block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to render + * @param array $context The context + * @param array $blocks The current set of blocks + * @param bool $useBlocks Whether to use the current set of blocks + * + * @return string The rendered block + * + * @internal + */ + public function renderBlock($name, array $context, array $blocks = array(), $useBlocks = true) + { + ob_start(); + $this->displayBlock($name, $context, $blocks, $useBlocks); + + return ob_get_clean(); + } + + /** + * Returns whether a block exists or not. + * + * This method is for internal use only and should never be called + * directly. + * + * This method does only return blocks defined in the current template + * or defined in "used" traits. + * + * It does not return blocks from parent templates as the parent + * template name can be dynamic, which is only known based on the + * current context. + * + * @param string $name The block name + * + * @return bool true if the block exists, false otherwise + * + * @internal + */ + public function hasBlock($name) + { + return isset($this->blocks[(string) $name]); + } + + /** + * Returns all block names. + * + * This method is for internal use only and should never be called + * directly. + * + * @return array An array of block names + * + * @see hasBlock + * + * @internal + */ + public function getBlockNames() + { + return array_keys($this->blocks); + } + + protected function loadTemplate($template, $templateName = null, $line = null, $index = null) + { + try { + if (is_array($template)) { + return $this->env->resolveTemplate($template); + } + + if ($template instanceof self) { + return $template; + } + + return $this->env->loadTemplate($template, $index); + } catch (Twig_Error $e) { + if (!$e->getTemplateFile()) { + $e->setTemplateFile($templateName ? $templateName : $this->getTemplateName()); + } + + if ($e->getTemplateLine()) { + throw $e; + } + + if (!$line) { + $e->guess(); + } else { + $e->setTemplateLine($line); + } + + throw $e; + } + } + + /** + * Returns all blocks. + * + * This method is for internal use only and should never be called + * directly. + * + * @return array An array of blocks + * + * @see hasBlock + * + * @internal + */ + public function getBlocks() + { + return $this->blocks; + } + + /** + * Returns the template source code. + * + * @return string|null The template source code or null if it is not available + */ + public function getSource() + { + $reflector = new ReflectionClass($this); + $file = $reflector->getFileName(); + + if (!file_exists($file)) { + return; + } + + $source = file($file, FILE_IGNORE_NEW_LINES); + array_splice($source, 0, $reflector->getEndLine()); + + $i = 0; + while (isset($source[$i]) && '/* */' === substr_replace($source[$i], '', 3, -2)) { + $source[$i] = str_replace('*//* ', '*/', substr($source[$i], 3, -2)); + ++$i; + } + array_splice($source, $i); + + return implode("\n", $source); + } + + /** + * {@inheritdoc} + */ + public function display(array $context, array $blocks = array()) + { + $this->displayWithErrorHandling($this->env->mergeGlobals($context), array_merge($this->blocks, $blocks)); + } + + /** + * {@inheritdoc} + */ + public function render(array $context) + { + $level = ob_get_level(); + ob_start(); + try { + $this->display($context); + } catch (Exception $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } catch (Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } + + return ob_get_clean(); + } + + protected function displayWithErrorHandling(array $context, array $blocks = array()) + { + try { + $this->doDisplay($context, $blocks); + } catch (Twig_Error $e) { + if (!$e->getTemplateFile()) { + $e->setTemplateFile($this->getTemplateName()); + } + + // this is mostly useful for Twig_Error_Loader exceptions + // see Twig_Error_Loader + if (false === $e->getTemplateLine()) { + $e->setTemplateLine(-1); + $e->guess(); + } + + throw $e; + } catch (Exception $e) { + throw new Twig_Error_Runtime(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getTemplateName(), $e); + } + } + + /** + * Auto-generated method to display the template with the given context. + * + * @param array $context An array of parameters to pass to the template + * @param array $blocks An array of blocks to pass to the template + */ + abstract protected function doDisplay(array $context, array $blocks = array()); + + /** + * Returns a variable from the context. + * + * This method is for internal use only and should never be called + * directly. + * + * This method should not be overridden in a sub-class as this is an + * implementation detail that has been introduced to optimize variable + * access for versions of PHP before 5.4. This is not a way to override + * the way to get a variable value. + * + * @param array $context The context + * @param string $item The variable to return from the context + * @param bool $ignoreStrictCheck Whether to ignore the strict variable check or not + * + * @return mixed The content of the context variable + * + * @throws Twig_Error_Runtime if the variable does not exist and Twig is running in strict mode + * + * @internal + */ + final protected function getContext($context, $item, $ignoreStrictCheck = false) + { + if (!array_key_exists($item, $context)) { + if ($ignoreStrictCheck || !$this->env->isStrictVariables()) { + return; + } + + throw new Twig_Error_Runtime(sprintf('Variable "%s" does not exist', $item), -1, $this->getTemplateName()); + } + + return $context[$item]; + } + + /** + * Returns the attribute value for a given array/object. + * + * @param mixed $object The object or array from where to get the item + * @param mixed $item The item to get from the array or object + * @param array $arguments An array of arguments to pass if the item is an object method + * @param string $type The type of attribute (@see Twig_Template constants) + * @param bool $isDefinedTest Whether this is only a defined check + * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not + * + * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true + * + * @throws Twig_Error_Runtime if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false + */ + protected function getAttribute($object, $item, array $arguments = array(), $type = self::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false) + { + // array + if (self::METHOD_CALL !== $type) { + $arrayItem = is_bool($item) || is_float($item) ? (int) $item : $item; + + if ((is_array($object) && array_key_exists($arrayItem, $object)) + || ($object instanceof ArrayAccess && isset($object[$arrayItem])) + ) { + if ($isDefinedTest) { + return true; + } + + return $object[$arrayItem]; + } + + if (self::ARRAY_CALL === $type || !is_object($object)) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$this->env->isStrictVariables()) { + return; + } + + if ($object instanceof ArrayAccess) { + $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist', $arrayItem, get_class($object)); + } elseif (is_object($object)) { + $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface', $item, get_class($object)); + } elseif (is_array($object)) { + if (empty($object)) { + $message = sprintf('Key "%s" does not exist as the array is empty', $arrayItem); + } else { + $message = sprintf('Key "%s" for array with keys "%s" does not exist', $arrayItem, implode(', ', array_keys($object))); + } + } elseif (self::ARRAY_CALL === $type) { + if (null === $object) { + $message = sprintf('Impossible to access a key ("%s") on a null variable', $item); + } else { + $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s")', $item, gettype($object), $object); + } + } elseif (null === $object) { + $message = sprintf('Impossible to access an attribute ("%s") on a null variable', $item); + } else { + $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s")', $item, gettype($object), $object); + } + + throw new Twig_Error_Runtime($message, -1, $this->getTemplateName()); + } + } + + if (!is_object($object)) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$this->env->isStrictVariables()) { + return; + } + + if (null === $object) { + $message = sprintf('Impossible to invoke a method ("%s") on a null variable', $item); + } else { + $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object); + } + + throw new Twig_Error_Runtime($message, -1, $this->getTemplateName()); + } + + // object property + if (self::METHOD_CALL !== $type && !$object instanceof self) { // Twig_Template does not have public properties, and we don't want to allow access to internal ones + if (isset($object->$item) || array_key_exists((string) $item, $object)) { + if ($isDefinedTest) { + return true; + } + + if ($this->env->hasExtension('sandbox')) { + $this->env->getExtension('sandbox')->checkPropertyAllowed($object, $item); + } + + return $object->$item; + } + } + + $class = get_class($object); + + // object method + if (!isset(self::$cache[$class]['methods'])) { + // get_class_methods returns all methods accessible in the scope, but we only want public ones to be accessible in templates + if ($object instanceof self) { + $ref = new ReflectionClass($class); + $methods = array(); + + foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $refMethod) { + $methodName = strtolower($refMethod->name); + + // Accessing the environment from templates is forbidden to prevent untrusted changes to the environment + if ('getenvironment' !== $methodName) { + $methods[$methodName] = true; + } + } + + self::$cache[$class]['methods'] = $methods; + } else { + self::$cache[$class]['methods'] = array_change_key_case(array_flip(get_class_methods($object))); + } + } + + $call = false; + $lcItem = strtolower($item); + if (isset(self::$cache[$class]['methods'][$lcItem])) { + $method = (string) $item; + } elseif (isset(self::$cache[$class]['methods']['get'.$lcItem])) { + $method = 'get'.$item; + } elseif (isset(self::$cache[$class]['methods']['is'.$lcItem])) { + $method = 'is'.$item; + } elseif (isset(self::$cache[$class]['methods']['__call'])) { + $method = (string) $item; + $call = true; + } else { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$this->env->isStrictVariables()) { + return; + } + + throw new Twig_Error_Runtime(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()" or "__call()" exist and have public access in class "%2$s"', $item, get_class($object)), -1, $this->getTemplateName()); + } + + if ($isDefinedTest) { + return true; + } + + if ($this->env->hasExtension('sandbox')) { + $this->env->getExtension('sandbox')->checkMethodAllowed($object, $method); + } + + // Some objects throw exceptions when they have __call, and the method we try + // to call is not supported. If ignoreStrictCheck is true, we should return null. + try { + $ret = call_user_func_array(array($object, $method), $arguments); + } catch (BadMethodCallException $e) { + if ($call && ($ignoreStrictCheck || !$this->env->isStrictVariables())) { + return; + } + throw $e; + } + + // useful when calling a template method from a template + // this is not supported but unfortunately heavily used in the Symfony profiler + if ($object instanceof Twig_TemplateInterface) { + return $ret === '' ? '' : new Twig_Markup($ret, $this->env->getCharset()); + } + + return $ret; + } +} diff --git a/public/system/library/template/Twig/TemplateInterface.php b/public/system/library/template/Twig/TemplateInterface.php new file mode 100644 index 0000000..3274640 --- /dev/null +++ b/public/system/library/template/Twig/TemplateInterface.php @@ -0,0 +1,48 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface implemented by all compiled templates. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 3.0) + */ +interface Twig_TemplateInterface +{ + const ANY_CALL = 'any'; + const ARRAY_CALL = 'array'; + const METHOD_CALL = 'method'; + + /** + * Renders the template with the given context and returns it as string. + * + * @param array $context An array of parameters to pass to the template + * + * @return string The rendered template + */ + public function render(array $context); + + /** + * Displays the template with the given context. + * + * @param array $context An array of parameters to pass to the template + * @param array $blocks An array of blocks to pass to the template + */ + public function display(array $context, array $blocks = array()); + + /** + * Returns the bound environment for this template. + * + * @return Twig_Environment The current environment + */ + public function getEnvironment(); +} diff --git a/public/system/library/template/Twig/Test.php b/public/system/library/template/Twig/Test.php new file mode 100644 index 0000000..3c2d859 --- /dev/null +++ b/public/system/library/template/Twig/Test.php @@ -0,0 +1,37 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Test class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleTest instead.', E_USER_DEPRECATED); + +/** + * Represents a template test. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +abstract class Twig_Test implements Twig_TestInterface, Twig_TestCallableInterface +{ + protected $options; + protected $arguments = array(); + + public function __construct(array $options = array()) + { + $this->options = array_merge(array( + 'callable' => null, + ), $options); + } + + public function getCallable() + { + return $this->options['callable']; + } +} diff --git a/public/system/library/template/Twig/Test/Function.php b/public/system/library/template/Twig/Test/Function.php new file mode 100644 index 0000000..5e76c71 --- /dev/null +++ b/public/system/library/template/Twig/Test/Function.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Test_Function class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleTest instead.', E_USER_DEPRECATED); + +/** + * Represents a function template test. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +class Twig_Test_Function extends Twig_Test +{ + protected $function; + + public function __construct($function, array $options = array()) + { + $options['callable'] = $function; + + parent::__construct($options); + + $this->function = $function; + } + + public function compile() + { + return $this->function; + } +} diff --git a/public/system/library/template/Twig/Test/IntegrationTestCase.php b/public/system/library/template/Twig/Test/IntegrationTestCase.php new file mode 100644 index 0000000..45ca7dc --- /dev/null +++ b/public/system/library/template/Twig/Test/IntegrationTestCase.php @@ -0,0 +1,232 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Integration test helper. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Karma Dordrak <drak@zikula.org> + */ +abstract class Twig_Test_IntegrationTestCase extends PHPUnit_Framework_TestCase +{ + /** + * @return string + */ + abstract protected function getFixturesDir(); + + /** + * @return Twig_ExtensionInterface[] + */ + protected function getExtensions() + { + return array(); + } + + /** + * @return Twig_SimpleFilter[] + */ + protected function getTwigFilters() + { + return array(); + } + + /** + * @return Twig_SimpleFunction[] + */ + protected function getTwigFunctions() + { + return array(); + } + + /** + * @return Twig_SimpleTest[] + */ + protected function getTwigTests() + { + return array(); + } + + /** + * @dataProvider getTests + */ + public function testIntegration($file, $message, $condition, $templates, $exception, $outputs) + { + $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs); + } + + /** + * @dataProvider getLegacyTests + * @group legacy + */ + public function testLegacyIntegration($file, $message, $condition, $templates, $exception, $outputs) + { + $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs); + } + + public function getTests($name, $legacyTests = false) + { + $fixturesDir = realpath($this->getFixturesDir()); + $tests = array(); + + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fixturesDir), RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if (!preg_match('/\.test$/', $file)) { + continue; + } + + if ($legacyTests xor false !== strpos($file->getRealpath(), '.legacy.test')) { + continue; + } + + $test = file_get_contents($file->getRealpath()); + + if (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)\s*(?:--DATA--\s*(.*))?\s*--EXCEPTION--\s*(.*)/sx', $test, $match)) { + $message = $match[1]; + $condition = $match[2]; + $templates = self::parseTemplates($match[3]); + $exception = $match[5]; + $outputs = array(array(null, $match[4], null, '')); + } elseif (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)--DATA--.*?--EXPECT--.*/s', $test, $match)) { + $message = $match[1]; + $condition = $match[2]; + $templates = self::parseTemplates($match[3]); + $exception = false; + preg_match_all('/--DATA--(.*?)(?:--CONFIG--(.*?))?--EXPECT--(.*?)(?=\-\-DATA\-\-|$)/s', $test, $outputs, PREG_SET_ORDER); + } else { + throw new InvalidArgumentException(sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); + } + + $tests[] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs); + } + + if ($legacyTests && empty($tests)) { + // add a dummy test to avoid a PHPUnit message + return array(array('not', '-', '', array(), '', array())); + } + + return $tests; + } + + public function getLegacyTests() + { + return $this->getTests('testLegacyIntegration', true); + } + + protected function doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs) + { + if ($condition) { + eval('$ret = '.$condition.';'); + if (!$ret) { + $this->markTestSkipped($condition); + } + } + + $loader = new Twig_Loader_Array($templates); + + foreach ($outputs as $i => $match) { + $config = array_merge(array( + 'cache' => false, + 'strict_variables' => true, + ), $match[2] ? eval($match[2].';') : array()); + $twig = new Twig_Environment($loader, $config); + $twig->addGlobal('global', 'global'); + foreach ($this->getExtensions() as $extension) { + $twig->addExtension($extension); + } + + foreach ($this->getTwigFilters() as $filter) { + $twig->addFilter($filter); + } + + foreach ($this->getTwigTests() as $test) { + $twig->addTest($test); + } + + foreach ($this->getTwigFunctions() as $function) { + $twig->addFunction($function); + } + + // avoid using the same PHP class name for different cases + // only for PHP 5.2+ + if (PHP_VERSION_ID >= 50300) { + $p = new ReflectionProperty($twig, 'templateClassPrefix'); + $p->setAccessible(true); + $p->setValue($twig, '__TwigTemplate_'.hash('sha256', uniqid(mt_rand(), true), false).'_'); + } + + try { + $template = $twig->loadTemplate('index.twig'); + } catch (Exception $e) { + if (false !== $exception) { + $message = $e->getMessage(); + $this->assertSame(trim($exception), trim(sprintf('%s: %s', get_class($e), $message))); + $this->assertSame('.', substr($message, strlen($message) - 1), $message, 'Exception message must end with a dot.'); + + return; + } + + if ($e instanceof Twig_Error_Syntax) { + $e->setTemplateFile($file); + + throw $e; + } + + throw new Twig_Error(sprintf('%s: %s', get_class($e), $e->getMessage()), -1, $file, $e); + } + + try { + $output = trim($template->render(eval($match[1].';')), "\n "); + } catch (Exception $e) { + if (false !== $exception) { + $this->assertSame(trim($exception), trim(sprintf('%s: %s', get_class($e), $e->getMessage()))); + + return; + } + + if ($e instanceof Twig_Error_Syntax) { + $e->setTemplateFile($file); + } else { + $e = new Twig_Error(sprintf('%s: %s', get_class($e), $e->getMessage()), -1, $file, $e); + } + + $output = trim(sprintf('%s: %s', get_class($e), $e->getMessage())); + } + + if (false !== $exception) { + list($class) = explode(':', $exception); + $this->assertThat(null, new PHPUnit_Framework_Constraint_Exception($class)); + } + + $expected = trim($match[3], "\n "); + + if ($expected !== $output) { + printf("Compiled templates that failed on case %d:\n", $i + 1); + + foreach (array_keys($templates) as $name) { + echo "Template: $name\n"; + $source = $loader->getSource($name); + echo $twig->compile($twig->parse($twig->tokenize($source, $name))); + } + } + $this->assertEquals($expected, $output, $message.' (in '.$file.')'); + } + } + + protected static function parseTemplates($test) + { + $templates = array(); + preg_match_all('/--TEMPLATE(?:\((.*?)\))?--(.*?)(?=\-\-TEMPLATE|$)/s', $test, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $templates[($match[1] ? $match[1] : 'index.twig')] = $match[2]; + } + + return $templates; + } +} diff --git a/public/system/library/template/Twig/Test/Method.php b/public/system/library/template/Twig/Test/Method.php new file mode 100644 index 0000000..2779986 --- /dev/null +++ b/public/system/library/template/Twig/Test/Method.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Test_Method class is deprecated since version 1.12 and will be removed in 2.0. Use Twig_SimpleTest instead.', E_USER_DEPRECATED); + +/** + * Represents a method template test. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +class Twig_Test_Method extends Twig_Test +{ + protected $extension; + protected $method; + + public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array()) + { + $options['callable'] = array($extension, $method); + + parent::__construct($options); + + $this->extension = $extension; + $this->method = $method; + } + + public function compile() + { + return sprintf('$this->env->getExtension(\'%s\')->%s', $this->extension->getName(), $this->method); + } +} diff --git a/public/system/library/template/Twig/Test/Node.php b/public/system/library/template/Twig/Test/Node.php new file mode 100644 index 0000000..baef49c --- /dev/null +++ b/public/system/library/template/Twig/Test/Node.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@trigger_error('The Twig_Test_Node class is deprecated since version 1.12 and will be removed in 2.0.', E_USER_DEPRECATED); + +/** + * Represents a template test as a Node. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +class Twig_Test_Node extends Twig_Test +{ + protected $class; + + public function __construct($class, array $options = array()) + { + parent::__construct($options); + + $this->class = $class; + } + + public function getClass() + { + return $this->class; + } + + public function compile() + { + } +} diff --git a/public/system/library/template/Twig/Test/NodeTestCase.php b/public/system/library/template/Twig/Test/NodeTestCase.php new file mode 100644 index 0000000..e591c1d --- /dev/null +++ b/public/system/library/template/Twig/Test/NodeTestCase.php @@ -0,0 +1,64 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +abstract class Twig_Test_NodeTestCase extends PHPUnit_Framework_TestCase +{ + abstract public function getTests(); + + /** + * @dataProvider getTests + */ + public function testCompile($node, $source, $environment = null, $isPattern = false) + { + $this->assertNodeCompilation($source, $node, $environment, $isPattern); + } + + public function assertNodeCompilation($source, Twig_Node $node, Twig_Environment $environment = null, $isPattern = false) + { + $compiler = $this->getCompiler($environment); + $compiler->compile($node); + + if ($isPattern) { + $this->assertStringMatchesFormat($source, trim($compiler->getSource())); + } else { + $this->assertEquals($source, trim($compiler->getSource())); + } + } + + protected function getCompiler(Twig_Environment $environment = null) + { + return new Twig_Compiler(null === $environment ? $this->getEnvironment() : $environment); + } + + protected function getEnvironment() + { + return new Twig_Environment(new Twig_Loader_Array(array())); + } + + protected function getVariableGetter($name, $line = false) + { + $line = $line > 0 ? "// line {$line}\n" : ''; + + if (PHP_VERSION_ID >= 50400) { + return sprintf('%s(isset($context["%s"]) ? $context["%s"] : null)', $line, $name, $name); + } + + return sprintf('%s$this->getContext($context, "%s")', $line, $name); + } + + protected function getAttributeGetter() + { + if (function_exists('twig_template_get_attributes')) { + return 'twig_template_get_attributes($this, '; + } + + return '$this->getAttribute('; + } +} diff --git a/public/system/library/template/Twig/TestCallableInterface.php b/public/system/library/template/Twig/TestCallableInterface.php new file mode 100644 index 0000000..98d3457 --- /dev/null +++ b/public/system/library/template/Twig/TestCallableInterface.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a callable template test. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_TestCallableInterface +{ + public function getCallable(); +} diff --git a/public/system/library/template/Twig/TestInterface.php b/public/system/library/template/Twig/TestInterface.php new file mode 100644 index 0000000..2fa821c --- /dev/null +++ b/public/system/library/template/Twig/TestInterface.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a template test. + * + * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_TestInterface +{ + /** + * Compiles a test. + * + * @return string The PHP code for the test + */ + public function compile(); +} diff --git a/public/system/library/template/Twig/Token.php b/public/system/library/template/Twig/Token.php new file mode 100644 index 0000000..a0a029b --- /dev/null +++ b/public/system/library/template/Twig/Token.php @@ -0,0 +1,216 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a Token. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Token +{ + protected $value; + protected $type; + protected $lineno; + + const EOF_TYPE = -1; + const TEXT_TYPE = 0; + const BLOCK_START_TYPE = 1; + const VAR_START_TYPE = 2; + const BLOCK_END_TYPE = 3; + const VAR_END_TYPE = 4; + const NAME_TYPE = 5; + const NUMBER_TYPE = 6; + const STRING_TYPE = 7; + const OPERATOR_TYPE = 8; + const PUNCTUATION_TYPE = 9; + const INTERPOLATION_START_TYPE = 10; + const INTERPOLATION_END_TYPE = 11; + + /** + * Constructor. + * + * @param int $type The type of the token + * @param string $value The token value + * @param int $lineno The line position in the source + */ + public function __construct($type, $value, $lineno) + { + $this->type = $type; + $this->value = $value; + $this->lineno = $lineno; + } + + /** + * Returns a string representation of the token. + * + * @return string A string representation of the token + */ + public function __toString() + { + return sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); + } + + /** + * Tests the current token for a type and/or a value. + * + * Parameters may be: + * * just type + * * type and value (or array of possible values) + * * just value (or array of possible values) (NAME_TYPE is used as type) + * + * @param array|int $type The type to test + * @param array|string|null $values The token value + * + * @return bool + */ + public function test($type, $values = null) + { + if (null === $values && !is_int($type)) { + $values = $type; + $type = self::NAME_TYPE; + } + + return ($this->type === $type) && ( + null === $values || + (is_array($values) && in_array($this->value, $values)) || + $this->value == $values + ); + } + + /** + * Gets the line. + * + * @return int The source line + */ + public function getLine() + { + return $this->lineno; + } + + /** + * Gets the token type. + * + * @return int The token type + */ + public function getType() + { + return $this->type; + } + + /** + * Gets the token value. + * + * @return string The token value + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns the constant representation (internal) of a given type. + * + * @param int $type The type as an integer + * @param bool $short Whether to return a short representation or not + * + * @return string The string representation + */ + public static function typeToString($type, $short = false) + { + switch ($type) { + case self::EOF_TYPE: + $name = 'EOF_TYPE'; + break; + case self::TEXT_TYPE: + $name = 'TEXT_TYPE'; + break; + case self::BLOCK_START_TYPE: + $name = 'BLOCK_START_TYPE'; + break; + case self::VAR_START_TYPE: + $name = 'VAR_START_TYPE'; + break; + case self::BLOCK_END_TYPE: + $name = 'BLOCK_END_TYPE'; + break; + case self::VAR_END_TYPE: + $name = 'VAR_END_TYPE'; + break; + case self::NAME_TYPE: + $name = 'NAME_TYPE'; + break; + case self::NUMBER_TYPE: + $name = 'NUMBER_TYPE'; + break; + case self::STRING_TYPE: + $name = 'STRING_TYPE'; + break; + case self::OPERATOR_TYPE: + $name = 'OPERATOR_TYPE'; + break; + case self::PUNCTUATION_TYPE: + $name = 'PUNCTUATION_TYPE'; + break; + case self::INTERPOLATION_START_TYPE: + $name = 'INTERPOLATION_START_TYPE'; + break; + case self::INTERPOLATION_END_TYPE: + $name = 'INTERPOLATION_END_TYPE'; + break; + default: + throw new LogicException(sprintf('Token of type "%s" does not exist.', $type)); + } + + return $short ? $name : 'Twig_Token::'.$name; + } + + /** + * Returns the english representation of a given type. + * + * @param int $type The type as an integer + * + * @return string The string representation + */ + public static function typeToEnglish($type) + { + switch ($type) { + case self::EOF_TYPE: + return 'end of template'; + case self::TEXT_TYPE: + return 'text'; + case self::BLOCK_START_TYPE: + return 'begin of statement block'; + case self::VAR_START_TYPE: + return 'begin of print statement'; + case self::BLOCK_END_TYPE: + return 'end of statement block'; + case self::VAR_END_TYPE: + return 'end of print statement'; + case self::NAME_TYPE: + return 'name'; + case self::NUMBER_TYPE: + return 'number'; + case self::STRING_TYPE: + return 'string'; + case self::OPERATOR_TYPE: + return 'operator'; + case self::PUNCTUATION_TYPE: + return 'punctuation'; + case self::INTERPOLATION_START_TYPE: + return 'begin of string interpolation'; + case self::INTERPOLATION_END_TYPE: + return 'end of string interpolation'; + default: + throw new LogicException(sprintf('Token of type "%s" does not exist.', $type)); + } + } +} diff --git a/public/system/library/template/Twig/TokenParser.php b/public/system/library/template/Twig/TokenParser.php new file mode 100644 index 0000000..fa9b6d8 --- /dev/null +++ b/public/system/library/template/Twig/TokenParser.php @@ -0,0 +1,33 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Base class for all token parsers. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +abstract class Twig_TokenParser implements Twig_TokenParserInterface +{ + /** + * @var Twig_Parser + */ + protected $parser; + + /** + * Sets the parser associated with this token parser. + * + * @param Twig_Parser $parser A Twig_Parser instance + */ + public function setParser(Twig_Parser $parser) + { + $this->parser = $parser; + } +} diff --git a/public/system/library/template/Twig/TokenParser/AutoEscape.php b/public/system/library/template/Twig/TokenParser/AutoEscape.php new file mode 100644 index 0000000..fd34caf --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/AutoEscape.php @@ -0,0 +1,79 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Marks a section of a template to be escaped or not. + * + * <pre> + * {% autoescape true %} + * Everything will be automatically escaped in this block + * {% endautoescape %} + * + * {% autoescape false %} + * Everything will be outputed as is in this block + * {% endautoescape %} + * + * {% autoescape true js %} + * Everything will be automatically escaped in this block + * using the js escaping strategy + * {% endautoescape %} + * </pre> + */ +class Twig_TokenParser_AutoEscape extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + + if ($stream->test(Twig_Token::BLOCK_END_TYPE)) { + $value = 'html'; + } else { + $expr = $this->parser->getExpressionParser()->parseExpression(); + if (!$expr instanceof Twig_Node_Expression_Constant) { + throw new Twig_Error_Syntax('An escaping strategy must be a string or a bool.', $stream->getCurrent()->getLine(), $stream->getFilename()); + } + $value = $expr->getAttribute('value'); + + $compat = true === $value || false === $value; + + if (true === $value) { + $value = 'html'; + } + + if ($compat && $stream->test(Twig_Token::NAME_TYPE)) { + @trigger_error('Using the autoescape tag with "true" or "false" before the strategy name is deprecated since version 1.21.', E_USER_DEPRECATED); + + if (false === $value) { + throw new Twig_Error_Syntax('Unexpected escaping strategy as you set autoescaping to false.', $stream->getCurrent()->getLine(), $stream->getFilename()); + } + + $value = $stream->next()->getValue(); + } + } + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_AutoEscape($value, $body, $lineno, $this->getTag()); + } + + public function decideBlockEnd(Twig_Token $token) + { + return $token->test('endautoescape'); + } + + public function getTag() + { + return 'autoescape'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Block.php b/public/system/library/template/Twig/TokenParser/Block.php new file mode 100644 index 0000000..4ffafbe --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Block.php @@ -0,0 +1,69 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Marks a section of a template as being reusable. + * + * <pre> + * {% block head %} + * <link rel="stylesheet" href="style.css" /> + * <title>{% block title %}{% endblock %} - My Webpage</title> + * {% endblock %} + * </pre> + */ +class Twig_TokenParser_Block extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); + if ($this->parser->hasBlock($name)) { + throw new Twig_Error_Syntax(sprintf("The block '%s' has already been defined line %d.", $name, $this->parser->getBlock($name)->getLine()), $stream->getCurrent()->getLine(), $stream->getFilename()); + } + $this->parser->setBlock($name, $block = new Twig_Node_Block($name, new Twig_Node(array()), $lineno)); + $this->parser->pushLocalScope(); + $this->parser->pushBlockStack($name); + + if ($stream->nextIf(Twig_Token::BLOCK_END_TYPE)) { + $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); + if ($token = $stream->nextIf(Twig_Token::NAME_TYPE)) { + $value = $token->getValue(); + + if ($value != $name) { + throw new Twig_Error_Syntax(sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getFilename()); + } + } + } else { + $body = new Twig_Node(array( + new Twig_Node_Print($this->parser->getExpressionParser()->parseExpression(), $lineno), + )); + } + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + $block->setNode('body', $body); + $this->parser->popBlockStack(); + $this->parser->popLocalScope(); + + return new Twig_Node_BlockReference($name, $lineno, $this->getTag()); + } + + public function decideBlockEnd(Twig_Token $token) + { + return $token->test('endblock'); + } + + public function getTag() + { + return 'block'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Do.php b/public/system/library/template/Twig/TokenParser/Do.php new file mode 100644 index 0000000..7adb5a0 --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Do.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Evaluates an expression, discarding the returned value. + */ +class Twig_TokenParser_Do extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $expr = $this->parser->getExpressionParser()->parseExpression(); + + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_Do($expr, $token->getLine(), $this->getTag()); + } + + public function getTag() + { + return 'do'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Embed.php b/public/system/library/template/Twig/TokenParser/Embed.php new file mode 100644 index 0000000..e685b95 --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Embed.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2012 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Embeds a template. + */ +class Twig_TokenParser_Embed extends Twig_TokenParser_Include +{ + public function parse(Twig_Token $token) + { + $stream = $this->parser->getStream(); + + $parent = $this->parser->getExpressionParser()->parseExpression(); + + list($variables, $only, $ignoreMissing) = $this->parseArguments(); + + // inject a fake parent to make the parent() function work + $stream->injectTokens(array( + new Twig_Token(Twig_Token::BLOCK_START_TYPE, '', $token->getLine()), + new Twig_Token(Twig_Token::NAME_TYPE, 'extends', $token->getLine()), + new Twig_Token(Twig_Token::STRING_TYPE, '__parent__', $token->getLine()), + new Twig_Token(Twig_Token::BLOCK_END_TYPE, '', $token->getLine()), + )); + + $module = $this->parser->parse($stream, array($this, 'decideBlockEnd'), true); + + // override the parent with the correct one + $module->setNode('parent', $parent); + + $this->parser->embedTemplate($module); + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_Embed($module->getAttribute('filename'), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + } + + public function decideBlockEnd(Twig_Token $token) + { + return $token->test('endembed'); + } + + public function getTag() + { + return 'embed'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Extends.php b/public/system/library/template/Twig/TokenParser/Extends.php new file mode 100644 index 0000000..510417a --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Extends.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Extends a template by another one. + * + * <pre> + * {% extends "base.html" %} + * </pre> + */ +class Twig_TokenParser_Extends extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + if (!$this->parser->isMainScope()) { + throw new Twig_Error_Syntax('Cannot extend from a block.', $token->getLine(), $this->parser->getFilename()); + } + + if (null !== $this->parser->getParent()) { + throw new Twig_Error_Syntax('Multiple extends tags are forbidden.', $token->getLine(), $this->parser->getFilename()); + } + $this->parser->setParent($this->parser->getExpressionParser()->parseExpression()); + + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + } + + public function getTag() + { + return 'extends'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Filter.php b/public/system/library/template/Twig/TokenParser/Filter.php new file mode 100644 index 0000000..b20dd5b --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Filter.php @@ -0,0 +1,49 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Filters a section of a template by applying filters. + * + * <pre> + * {% filter upper %} + * This text becomes uppercase + * {% endfilter %} + * </pre> + */ +class Twig_TokenParser_Filter extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $name = $this->parser->getVarName(); + $ref = new Twig_Node_Expression_BlockReference(new Twig_Node_Expression_Constant($name, $token->getLine()), true, $token->getLine(), $this->getTag()); + + $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref, $this->getTag()); + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + $block = new Twig_Node_Block($name, $body, $token->getLine()); + $this->parser->setBlock($name, $block); + + return new Twig_Node_Print($filter, $token->getLine(), $this->getTag()); + } + + public function decideBlockEnd(Twig_Token $token) + { + return $token->test('endfilter'); + } + + public function getTag() + { + return 'filter'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Flush.php b/public/system/library/template/Twig/TokenParser/Flush.php new file mode 100644 index 0000000..f9ce7c3 --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Flush.php @@ -0,0 +1,30 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Flushes the output to the client. + * + * @see flush() + */ +class Twig_TokenParser_Flush extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_Flush($token->getLine(), $this->getTag()); + } + + public function getTag() + { + return 'flush'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/For.php b/public/system/library/template/Twig/TokenParser/For.php new file mode 100644 index 0000000..3fac511 --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/For.php @@ -0,0 +1,123 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Loops over each item of a sequence. + * + * <pre> + * <ul> + * {% for user in users %} + * <li>{{ user.username|e }}</li> + * {% endfor %} + * </ul> + * </pre> + */ +class Twig_TokenParser_For extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + $targets = $this->parser->getExpressionParser()->parseAssignmentExpression(); + $stream->expect(Twig_Token::OPERATOR_TYPE, 'in'); + $seq = $this->parser->getExpressionParser()->parseExpression(); + + $ifexpr = null; + if ($stream->nextIf(Twig_Token::NAME_TYPE, 'if')) { + $ifexpr = $this->parser->getExpressionParser()->parseExpression(); + } + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideForFork')); + if ($stream->next()->getValue() == 'else') { + $stream->expect(Twig_Token::BLOCK_END_TYPE); + $else = $this->parser->subparse(array($this, 'decideForEnd'), true); + } else { + $else = null; + } + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + if (count($targets) > 1) { + $keyTarget = $targets->getNode(0); + $keyTarget = new Twig_Node_Expression_AssignName($keyTarget->getAttribute('name'), $keyTarget->getLine()); + $valueTarget = $targets->getNode(1); + $valueTarget = new Twig_Node_Expression_AssignName($valueTarget->getAttribute('name'), $valueTarget->getLine()); + } else { + $keyTarget = new Twig_Node_Expression_AssignName('_key', $lineno); + $valueTarget = $targets->getNode(0); + $valueTarget = new Twig_Node_Expression_AssignName($valueTarget->getAttribute('name'), $valueTarget->getLine()); + } + + if ($ifexpr) { + $this->checkLoopUsageCondition($stream, $ifexpr); + $this->checkLoopUsageBody($stream, $body); + } + + return new Twig_Node_For($keyTarget, $valueTarget, $seq, $ifexpr, $body, $else, $lineno, $this->getTag()); + } + + public function decideForFork(Twig_Token $token) + { + return $token->test(array('else', 'endfor')); + } + + public function decideForEnd(Twig_Token $token) + { + return $token->test('endfor'); + } + + // the loop variable cannot be used in the condition + protected function checkLoopUsageCondition(Twig_TokenStream $stream, Twig_NodeInterface $node) + { + if ($node instanceof Twig_Node_Expression_GetAttr && $node->getNode('node') instanceof Twig_Node_Expression_Name && 'loop' == $node->getNode('node')->getAttribute('name')) { + throw new Twig_Error_Syntax('The "loop" variable cannot be used in a looping condition.', $node->getLine(), $stream->getFilename()); + } + + foreach ($node as $n) { + if (!$n) { + continue; + } + + $this->checkLoopUsageCondition($stream, $n); + } + } + + // check usage of non-defined loop-items + // it does not catch all problems (for instance when a for is included into another or when the variable is used in an include) + protected function checkLoopUsageBody(Twig_TokenStream $stream, Twig_NodeInterface $node) + { + if ($node instanceof Twig_Node_Expression_GetAttr && $node->getNode('node') instanceof Twig_Node_Expression_Name && 'loop' == $node->getNode('node')->getAttribute('name')) { + $attribute = $node->getNode('attribute'); + if ($attribute instanceof Twig_Node_Expression_Constant && in_array($attribute->getAttribute('value'), array('length', 'revindex0', 'revindex', 'last'))) { + throw new Twig_Error_Syntax(sprintf('The "loop.%s" variable is not defined when looping with a condition.', $attribute->getAttribute('value')), $node->getLine(), $stream->getFilename()); + } + } + + // should check for parent.loop.XXX usage + if ($node instanceof Twig_Node_For) { + return; + } + + foreach ($node as $n) { + if (!$n) { + continue; + } + + $this->checkLoopUsageBody($stream, $n); + } + } + + public function getTag() + { + return 'for'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/From.php b/public/system/library/template/Twig/TokenParser/From.php new file mode 100644 index 0000000..f7547eb --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/From.php @@ -0,0 +1,62 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Imports macros. + * + * <pre> + * {% from 'forms.html' import forms %} + * </pre> + */ +class Twig_TokenParser_From extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $macro = $this->parser->getExpressionParser()->parseExpression(); + $stream = $this->parser->getStream(); + $stream->expect('import'); + + $targets = array(); + do { + $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); + + $alias = $name; + if ($stream->nextIf('as')) { + $alias = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); + } + + $targets[$name] = $alias; + + if (!$stream->nextIf(Twig_Token::PUNCTUATION_TYPE, ',')) { + break; + } + } while (true); + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + $node = new Twig_Node_Import($macro, new Twig_Node_Expression_AssignName($this->parser->getVarName(), $token->getLine()), $token->getLine(), $this->getTag()); + + foreach ($targets as $name => $alias) { + if ($this->parser->isReservedMacroName($name)) { + throw new Twig_Error_Syntax(sprintf('"%s" cannot be an imported macro as it is a reserved keyword.', $name), $token->getLine(), $stream->getFilename()); + } + + $this->parser->addImportedSymbol('function', $alias, 'get'.$name, $node->getNode('var')); + } + + return $node; + } + + public function getTag() + { + return 'from'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/If.php b/public/system/library/template/Twig/TokenParser/If.php new file mode 100644 index 0000000..91c0604 --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/If.php @@ -0,0 +1,82 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Tests a condition. + * + * <pre> + * {% if users %} + * <ul> + * {% for user in users %} + * <li>{{ user.username|e }}</li> + * {% endfor %} + * </ul> + * {% endif %} + * </pre> + */ +class Twig_TokenParser_If extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $lineno = $token->getLine(); + $expr = $this->parser->getExpressionParser()->parseExpression(); + $stream = $this->parser->getStream(); + $stream->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideIfFork')); + $tests = array($expr, $body); + $else = null; + + $end = false; + while (!$end) { + switch ($stream->next()->getValue()) { + case 'else': + $stream->expect(Twig_Token::BLOCK_END_TYPE); + $else = $this->parser->subparse(array($this, 'decideIfEnd')); + break; + + case 'elseif': + $expr = $this->parser->getExpressionParser()->parseExpression(); + $stream->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideIfFork')); + $tests[] = $expr; + $tests[] = $body; + break; + + case 'endif': + $end = true; + break; + + default: + throw new Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getFilename()); + } + } + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_If(new Twig_Node($tests), $else, $lineno, $this->getTag()); + } + + public function decideIfFork(Twig_Token $token) + { + return $token->test(array('elseif', 'else', 'endif')); + } + + public function decideIfEnd(Twig_Token $token) + { + return $token->test(array('endif')); + } + + public function getTag() + { + return 'if'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Import.php b/public/system/library/template/Twig/TokenParser/Import.php new file mode 100644 index 0000000..85c5c03 --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Import.php @@ -0,0 +1,37 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Imports macros. + * + * <pre> + * {% import 'forms.html' as forms %} + * </pre> + */ +class Twig_TokenParser_Import extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $macro = $this->parser->getExpressionParser()->parseExpression(); + $this->parser->getStream()->expect('as'); + $var = new Twig_Node_Expression_AssignName($this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue(), $token->getLine()); + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + $this->parser->addImportedSymbol('template', $var->getAttribute('name')); + + return new Twig_Node_Import($macro, $var, $token->getLine(), $this->getTag()); + } + + public function getTag() + { + return 'import'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Include.php b/public/system/library/template/Twig/TokenParser/Include.php new file mode 100644 index 0000000..0e76dae --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Include.php @@ -0,0 +1,63 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Includes a template. + * + * <pre> + * {% include 'header.html' %} + * Body + * {% include 'footer.html' %} + * </pre> + */ +class Twig_TokenParser_Include extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $expr = $this->parser->getExpressionParser()->parseExpression(); + + list($variables, $only, $ignoreMissing) = $this->parseArguments(); + + return new Twig_Node_Include($expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + } + + protected function parseArguments() + { + $stream = $this->parser->getStream(); + + $ignoreMissing = false; + if ($stream->nextIf(Twig_Token::NAME_TYPE, 'ignore')) { + $stream->expect(Twig_Token::NAME_TYPE, 'missing'); + + $ignoreMissing = true; + } + + $variables = null; + if ($stream->nextIf(Twig_Token::NAME_TYPE, 'with')) { + $variables = $this->parser->getExpressionParser()->parseExpression(); + } + + $only = false; + if ($stream->nextIf(Twig_Token::NAME_TYPE, 'only')) { + $only = true; + } + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + return array($variables, $only, $ignoreMissing); + } + + public function getTag() + { + return 'include'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Macro.php b/public/system/library/template/Twig/TokenParser/Macro.php new file mode 100644 index 0000000..8a7ebd6 --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Macro.php @@ -0,0 +1,56 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Defines a macro. + * + * <pre> + * {% macro input(name, value, type, size) %} + * <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" /> + * {% endmacro %} + * </pre> + */ +class Twig_TokenParser_Macro extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); + + $arguments = $this->parser->getExpressionParser()->parseArguments(true, true); + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + $this->parser->pushLocalScope(); + $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); + if ($token = $stream->nextIf(Twig_Token::NAME_TYPE)) { + $value = $token->getValue(); + + if ($value != $name) { + throw new Twig_Error_Syntax(sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getFilename()); + } + } + $this->parser->popLocalScope(); + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + $this->parser->setMacro($name, new Twig_Node_Macro($name, new Twig_Node_Body(array($body)), $arguments, $lineno, $this->getTag())); + } + + public function decideBlockEnd(Twig_Token $token) + { + return $token->test('endmacro'); + } + + public function getTag() + { + return 'macro'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Sandbox.php b/public/system/library/template/Twig/TokenParser/Sandbox.php new file mode 100644 index 0000000..1feadd0 --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Sandbox.php @@ -0,0 +1,56 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Marks a section of a template as untrusted code that must be evaluated in the sandbox mode. + * + * <pre> + * {% sandbox %} + * {% include 'user.html' %} + * {% endsandbox %} + * </pre> + * + * @see http://www.twig-project.org/doc/api.html#sandbox-extension for details + */ +class Twig_TokenParser_Sandbox extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + // in a sandbox tag, only include tags are allowed + if (!$body instanceof Twig_Node_Include) { + foreach ($body as $node) { + if ($node instanceof Twig_Node_Text && ctype_space($node->getAttribute('data'))) { + continue; + } + + if (!$node instanceof Twig_Node_Include) { + throw new Twig_Error_Syntax('Only "include" tags are allowed within a "sandbox" section.', $node->getLine(), $this->parser->getFilename()); + } + } + } + + return new Twig_Node_Sandbox($body, $token->getLine(), $this->getTag()); + } + + public function decideBlockEnd(Twig_Token $token) + { + return $token->test('endsandbox'); + } + + public function getTag() + { + return 'sandbox'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Set.php b/public/system/library/template/Twig/TokenParser/Set.php new file mode 100644 index 0000000..5ca614b --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Set.php @@ -0,0 +1,71 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Defines a variable. + * + * <pre> + * {% set foo = 'foo' %} + * + * {% set foo = [1, 2] %} + * + * {% set foo = {'foo': 'bar'} %} + * + * {% set foo = 'foo' ~ 'bar' %} + * + * {% set foo, bar = 'foo', 'bar' %} + * + * {% set foo %}Some content{% endset %} + * </pre> + */ +class Twig_TokenParser_Set extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + $names = $this->parser->getExpressionParser()->parseAssignmentExpression(); + + $capture = false; + if ($stream->nextIf(Twig_Token::OPERATOR_TYPE, '=')) { + $values = $this->parser->getExpressionParser()->parseMultitargetExpression(); + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + if (count($names) !== count($values)) { + throw new Twig_Error_Syntax('When using set, you must have the same number of variables and assignments.', $stream->getCurrent()->getLine(), $stream->getFilename()); + } + } else { + $capture = true; + + if (count($names) > 1) { + throw new Twig_Error_Syntax('When using set with a block, you cannot have a multi-target.', $stream->getCurrent()->getLine(), $stream->getFilename()); + } + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + $values = $this->parser->subparse(array($this, 'decideBlockEnd'), true); + $stream->expect(Twig_Token::BLOCK_END_TYPE); + } + + return new Twig_Node_Set($capture, $names, $values, $lineno, $this->getTag()); + } + + public function decideBlockEnd(Twig_Token $token) + { + return $token->test('endset'); + } + + public function getTag() + { + return 'set'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Spaceless.php b/public/system/library/template/Twig/TokenParser/Spaceless.php new file mode 100644 index 0000000..53d906d --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Spaceless.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Remove whitespaces between HTML tags. + * + * <pre> + * {% spaceless %} + * <div> + * <strong>foo</strong> + * </div> + * {% endspaceless %} + * + * {# output will be <div><strong>foo</strong></div> #} + * </pre> + */ +class Twig_TokenParser_Spaceless extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $lineno = $token->getLine(); + + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideSpacelessEnd'), true); + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_Spaceless($body, $lineno, $this->getTag()); + } + + public function decideSpacelessEnd(Twig_Token $token) + { + return $token->test('endspaceless'); + } + + public function getTag() + { + return 'spaceless'; + } +} diff --git a/public/system/library/template/Twig/TokenParser/Use.php b/public/system/library/template/Twig/TokenParser/Use.php new file mode 100644 index 0000000..4945d03 --- /dev/null +++ b/public/system/library/template/Twig/TokenParser/Use.php @@ -0,0 +1,64 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2011 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Imports blocks defined in another template into the current template. + * + * <pre> + * {% extends "base.html" %} + * + * {% use "blocks.html" %} + * + * {% block title %}{% endblock %} + * {% block content %}{% endblock %} + * </pre> + * + * @see http://www.twig-project.org/doc/templates.html#horizontal-reuse for details. + */ +class Twig_TokenParser_Use extends Twig_TokenParser +{ + public function parse(Twig_Token $token) + { + $template = $this->parser->getExpressionParser()->parseExpression(); + $stream = $this->parser->getStream(); + + if (!$template instanceof Twig_Node_Expression_Constant) { + throw new Twig_Error_Syntax('The template references in a "use" statement must be a string.', $stream->getCurrent()->getLine(), $stream->getFilename()); + } + + $targets = array(); + if ($stream->nextIf('with')) { + do { + $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); + + $alias = $name; + if ($stream->nextIf('as')) { + $alias = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); + } + + $targets[$name] = new Twig_Node_Expression_Constant($alias, -1); + + if (!$stream->nextIf(Twig_Token::PUNCTUATION_TYPE, ',')) { + break; + } + } while (true); + } + + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + $this->parser->addTrait(new Twig_Node(array('template' => $template, 'targets' => new Twig_Node($targets)))); + } + + public function getTag() + { + return 'use'; + } +} diff --git a/public/system/library/template/Twig/TokenParserBroker.php b/public/system/library/template/Twig/TokenParserBroker.php new file mode 100644 index 0000000..d88bb43 --- /dev/null +++ b/public/system/library/template/Twig/TokenParserBroker.php @@ -0,0 +1,142 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * (c) 2010 Arnaud Le Blanc + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Default implementation of a token parser broker. + * + * @author Arnaud Le Blanc <arnaud.lb@gmail.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +class Twig_TokenParserBroker implements Twig_TokenParserBrokerInterface +{ + protected $parser; + protected $parsers = array(); + protected $brokers = array(); + + /** + * Constructor. + * + * @param array|Traversable $parsers A Traversable of Twig_TokenParserInterface instances + * @param array|Traversable $brokers A Traversable of Twig_TokenParserBrokerInterface instances + * @param bool $triggerDeprecationError + */ + public function __construct($parsers = array(), $brokers = array(), $triggerDeprecationError = true) + { + if ($triggerDeprecationError) { + @trigger_error('The '.__CLASS__.' class is deprecated since version 1.12 and will be removed in 2.0.', E_USER_DEPRECATED); + } + + foreach ($parsers as $parser) { + if (!$parser instanceof Twig_TokenParserInterface) { + throw new LogicException('$parsers must a an array of Twig_TokenParserInterface.'); + } + $this->parsers[$parser->getTag()] = $parser; + } + foreach ($brokers as $broker) { + if (!$broker instanceof Twig_TokenParserBrokerInterface) { + throw new LogicException('$brokers must a an array of Twig_TokenParserBrokerInterface.'); + } + $this->brokers[] = $broker; + } + } + + /** + * Adds a TokenParser. + * + * @param Twig_TokenParserInterface $parser A Twig_TokenParserInterface instance + */ + public function addTokenParser(Twig_TokenParserInterface $parser) + { + $this->parsers[$parser->getTag()] = $parser; + } + + /** + * Removes a TokenParser. + * + * @param Twig_TokenParserInterface $parser A Twig_TokenParserInterface instance + */ + public function removeTokenParser(Twig_TokenParserInterface $parser) + { + $name = $parser->getTag(); + if (isset($this->parsers[$name]) && $parser === $this->parsers[$name]) { + unset($this->parsers[$name]); + } + } + + /** + * Adds a TokenParserBroker. + * + * @param Twig_TokenParserBroker $broker A Twig_TokenParserBroker instance + */ + public function addTokenParserBroker(Twig_TokenParserBroker $broker) + { + $this->brokers[] = $broker; + } + + /** + * Removes a TokenParserBroker. + * + * @param Twig_TokenParserBroker $broker A Twig_TokenParserBroker instance + */ + public function removeTokenParserBroker(Twig_TokenParserBroker $broker) + { + if (false !== $pos = array_search($broker, $this->brokers)) { + unset($this->brokers[$pos]); + } + } + + /** + * Gets a suitable TokenParser for a tag. + * + * First looks in parsers, then in brokers. + * + * @param string $tag A tag name + * + * @return null|Twig_TokenParserInterface A Twig_TokenParserInterface or null if no suitable TokenParser was found + */ + public function getTokenParser($tag) + { + if (isset($this->parsers[$tag])) { + return $this->parsers[$tag]; + } + $broker = end($this->brokers); + while (false !== $broker) { + $parser = $broker->getTokenParser($tag); + if (null !== $parser) { + return $parser; + } + $broker = prev($this->brokers); + } + } + + public function getParsers() + { + return $this->parsers; + } + + public function getParser() + { + return $this->parser; + } + + public function setParser(Twig_ParserInterface $parser) + { + $this->parser = $parser; + foreach ($this->parsers as $tokenParser) { + $tokenParser->setParser($parser); + } + foreach ($this->brokers as $broker) { + $broker->setParser($parser); + } + } +} diff --git a/public/system/library/template/Twig/TokenParserBrokerInterface.php b/public/system/library/template/Twig/TokenParserBrokerInterface.php new file mode 100644 index 0000000..3ec2a88 --- /dev/null +++ b/public/system/library/template/Twig/TokenParserBrokerInterface.php @@ -0,0 +1,46 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * (c) 2010 Arnaud Le Blanc + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface implemented by token parser brokers. + * + * Token parser brokers allows to implement custom logic in the process of resolving a token parser for a given tag name. + * + * @author Arnaud Le Blanc <arnaud.lb@gmail.com> + * + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_TokenParserBrokerInterface +{ + /** + * Gets a TokenParser suitable for a tag. + * + * @param string $tag A tag name + * + * @return null|Twig_TokenParserInterface A Twig_TokenParserInterface or null if no suitable TokenParser was found + */ + public function getTokenParser($tag); + + /** + * Calls Twig_TokenParserInterface::setParser on all parsers the implementation knows of. + * + * @param Twig_ParserInterface $parser A Twig_ParserInterface interface + */ + public function setParser(Twig_ParserInterface $parser); + + /** + * Gets the Twig_ParserInterface. + * + * @return null|Twig_ParserInterface A Twig_ParserInterface instance or null + */ + public function getParser(); +} diff --git a/public/system/library/template/Twig/TokenParserInterface.php b/public/system/library/template/Twig/TokenParserInterface.php new file mode 100644 index 0000000..12ec396 --- /dev/null +++ b/public/system/library/template/Twig/TokenParserInterface.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2010 Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Interface implemented by token parsers. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +interface Twig_TokenParserInterface +{ + /** + * Sets the parser associated with this token parser. + * + * @param Twig_Parser $parser A Twig_Parser instance + */ + public function setParser(Twig_Parser $parser); + + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + * + * @throws Twig_Error_Syntax + */ + public function parse(Twig_Token $token); + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag(); +} diff --git a/public/system/library/template/Twig/TokenStream.php b/public/system/library/template/Twig/TokenStream.php new file mode 100644 index 0000000..016f812 --- /dev/null +++ b/public/system/library/template/Twig/TokenStream.php @@ -0,0 +1,155 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) 2009 Fabien Potencier + * (c) 2009 Armin Ronacher + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Represents a token stream. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_TokenStream +{ + protected $tokens; + protected $current = 0; + protected $filename; + + /** + * Constructor. + * + * @param array $tokens An array of tokens + * @param string $filename The name of the filename which tokens are associated with + */ + public function __construct(array $tokens, $filename = null) + { + $this->tokens = $tokens; + $this->filename = $filename; + } + + /** + * Returns a string representation of the token stream. + * + * @return string + */ + public function __toString() + { + return implode("\n", $this->tokens); + } + + public function injectTokens(array $tokens) + { + $this->tokens = array_merge(array_slice($this->tokens, 0, $this->current), $tokens, array_slice($this->tokens, $this->current)); + } + + /** + * Sets the pointer to the next token and returns the old one. + * + * @return Twig_Token + */ + public function next() + { + if (!isset($this->tokens[++$this->current])) { + throw new Twig_Error_Syntax('Unexpected end of template.', $this->tokens[$this->current - 1]->getLine(), $this->filename); + } + + return $this->tokens[$this->current - 1]; + } + + /** + * Tests a token, sets the pointer to the next one and returns it or throws a syntax error. + * + * @return Twig_Token|null The next token if the condition is true, null otherwise + */ + public function nextIf($primary, $secondary = null) + { + if ($this->tokens[$this->current]->test($primary, $secondary)) { + return $this->next(); + } + } + + /** + * Tests a token and returns it or throws a syntax error. + * + * @return Twig_Token + */ + public function expect($type, $value = null, $message = null) + { + $token = $this->tokens[$this->current]; + if (!$token->test($type, $value)) { + $line = $token->getLine(); + throw new Twig_Error_Syntax(sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s).', + $message ? $message.'. ' : '', + Twig_Token::typeToEnglish($token->getType()), $token->getValue(), + Twig_Token::typeToEnglish($type), $value ? sprintf(' with value "%s"', $value) : ''), + $line, + $this->filename + ); + } + $this->next(); + + return $token; + } + + /** + * Looks at the next token. + * + * @param int $number + * + * @return Twig_Token + */ + public function look($number = 1) + { + if (!isset($this->tokens[$this->current + $number])) { + throw new Twig_Error_Syntax('Unexpected end of template.', $this->tokens[$this->current + $number - 1]->getLine(), $this->filename); + } + + return $this->tokens[$this->current + $number]; + } + + /** + * Tests the current token. + * + * @return bool + */ + public function test($primary, $secondary = null) + { + return $this->tokens[$this->current]->test($primary, $secondary); + } + + /** + * Checks if end of stream was reached. + * + * @return bool + */ + public function isEOF() + { + return $this->tokens[$this->current]->getType() === Twig_Token::EOF_TYPE; + } + + /** + * Gets the current token. + * + * @return Twig_Token + */ + public function getCurrent() + { + return $this->tokens[$this->current]; + } + + /** + * Gets the filename associated with this stream. + * + * @return string + */ + public function getFilename() + { + return $this->filename; + } +} diff --git a/public/system/library/template/Twig/Util/DeprecationCollector.php b/public/system/library/template/Twig/Util/DeprecationCollector.php new file mode 100644 index 0000000..e406f0a --- /dev/null +++ b/public/system/library/template/Twig/Util/DeprecationCollector.php @@ -0,0 +1,82 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Util_DeprecationCollector +{ + private $twig; + private $deprecations; + + public function __construct(Twig_Environment $twig) + { + $this->twig = $twig; + } + + /** + * Returns deprecations for templates contained in a directory. + * + * @param string $dir A directory where templates are stored + * @param string $ext Limit the loaded templates by extension + * + * @return array() An array of deprecations + */ + public function collectDir($dir, $ext = '.twig') + { + $iterator = new RegexIterator( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::LEAVES_ONLY + ), '{'.preg_quote($ext).'$}' + ); + + return $this->collect(new Twig_Util_TemplateDirIterator($iterator)); + } + + /** + * Returns deprecations for passed templates. + * + * @param Iterator $iterator An iterator of templates (where keys are template names and values the contents of the template) + * + * @return array() An array of deprecations + */ + public function collect(Iterator $iterator) + { + $this->deprecations = array(); + + set_error_handler(array($this, 'errorHandler')); + + foreach ($iterator as $name => $contents) { + try { + $this->twig->parse($this->twig->tokenize($contents, $name)); + } catch (Twig_Error_Syntax $e) { + // ignore templates containing syntax errors + } + } + + restore_error_handler(); + + $deprecations = $this->deprecations; + $this->deprecations = array(); + + return $deprecations; + } + + /** + * @internal + */ + public function errorHandler($type, $msg) + { + if (E_USER_DEPRECATED === $type) { + $this->deprecations[] = $msg; + } + } +} diff --git a/public/system/library/template/Twig/Util/TemplateDirIterator.php b/public/system/library/template/Twig/Util/TemplateDirIterator.php new file mode 100644 index 0000000..3fb8932 --- /dev/null +++ b/public/system/library/template/Twig/Util/TemplateDirIterator.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of Twig. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Fabien Potencier <fabien@symfony.com> + */ +class Twig_Util_TemplateDirIterator extends IteratorIterator +{ + public function current() + { + return file_get_contents(parent::current()); + } + + public function key() + { + return (string) parent::key(); + } +} diff --git a/public/system/library/template/template.php b/public/system/library/template/template.php new file mode 100644 index 0000000..0c3225b --- /dev/null +++ b/public/system/library/template/template.php @@ -0,0 +1,26 @@ +<?php +namespace Template; +final class Template { + private $data = array(); + + public function set($key, $value) { + $this->data[$key] = $value; + } + + public function render($template) { + $file = DIR_TEMPLATE . $template . '.tpl'; + + if (is_file($file)) { + extract($this->data); + + ob_start(); + + require($file); + + return ob_get_clean(); + } + + throw new \Exception('Error: Could not load template ' . $file . '!'); + exit(); + } +} diff --git a/public/system/library/template/twig.php b/public/system/library/template/twig.php new file mode 100644 index 0000000..e60c2a5 --- /dev/null +++ b/public/system/library/template/twig.php @@ -0,0 +1,41 @@ +<?php +namespace Template; +final class Twig { + private $twig; + private $data = array(); + + public function __construct() { + // include and register Twig auto-loader + include_once(DIR_SYSTEM . 'library/template/Twig/Autoloader.php'); + + \Twig_Autoloader::register(); + } + + public function set($key, $value) { + $this->data[$key] = $value; + } + + public function render($template, $cache = false) { + // specify where to look for templates + $loader = new \Twig_Loader_Filesystem(DIR_TEMPLATE); + + // initialize Twig environment + $config = array('autoescape' => false); + + if ($cache) { + $config['cache'] = DIR_CACHE; + } + + $this->twig = new \Twig_Environment($loader, $config); + + try { + // load template + $template = $this->twig->loadTemplate($template . '.twig'); + + return $template->render($this->data); + } catch (Exception $e) { + trigger_error('Error: Could not load template ' . $template . '!'); + exit(); + } + } +} diff --git a/public/system/library/url.php b/public/system/library/url.php new file mode 100644 index 0000000..92dae5a --- /dev/null +++ b/public/system/library/url.php @@ -0,0 +1,69 @@ +<?php +/** + * @package OpenCart + * @author Daniel Kerr + * @copyright Copyright (c) 2005 - 2017, OpenCart, Ltd. (https://www.opencart.com/) + * @license https://opensource.org/licenses/GPL-3.0 + * @link https://www.opencart.com +*/ + +/** +* URL class +*/ +class Url { + private $url; + private $ssl; + private $rewrite = array(); + + /** + * Constructor + * + * @param string $url + * @param string $ssl + * + */ + public function __construct($url, $ssl = '') { + $this->url = $url; + $this->ssl = $ssl; + } + + /** + * + * + * @param object $rewrite + */ + public function addRewrite($rewrite) { + $this->rewrite[] = $rewrite; + } + + /** + * + * + * @param string $route + * @param mixed $args + * @param bool $secure + * + * @return string + */ + public function link($route, $args = '', $secure = false) { + if ($this->ssl && $secure) { + $url = $this->ssl . 'index.php?route=' . $route; + } else { + $url = $this->url . 'index.php?route=' . $route; + } + + if ($args) { + if (is_array($args)) { + $url .= '&' . http_build_query($args); + } else { + $url .= str_replace('&', '&', '&' . ltrim($args, '&')); + } + } + + foreach ($this->rewrite as $rewrite) { + $url = $rewrite->rewrite($url); + } + + return $url; + } +}
\ No newline at end of file |