diff --git a/bin/grav b/bin/grav new file mode 100755 index 000000000..1bc34ca15 --- /dev/null +++ b/bin/grav @@ -0,0 +1,21 @@ +#!/usr/bin/env php +addCommands(array( + new Grav\Console\InstallCommand(), + new Grav\Console\PackageCommand(), +)); +$app->run(); diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..c937e27a8 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "rhuk/grav", + "type": "library", + "description": "Grav is a powerful flat CMS influenced by Pico, Stacey, Kirby and others...", + "keywords": ["cms"], + "homepage": "http://getgrav.org", + "license": "MIT", + "authors": [ + { + "name": "Andy Miller", + "email": "rhuk@getgrav.org" + } + ], + "require": { + "php": ">=5.3.10", + "twig/twig": "1.16.*@dev", + "erusev/parsedown": "dev-master", + "symfony/yaml": "2.5.*@dev", + "symfony/console": "2.5.*@dev", + "doctrine/cache": "1.4.*@dev", + "tracy/tracy": "dev-master", + "gregwar/image": "dev-master", + "ircmaxell/password-compat": "1.0.*" + } +} diff --git a/index.php b/index.php new file mode 100644 index 000000000..fda4afaba --- /dev/null +++ b/index.php @@ -0,0 +1,42 @@ +store('Grav', new Grav); +$registry->store('Uri', new Uri); +$registry->store('Config', Config::instance(CACHE_DIR . 'config.php')); +$registry->store('Cache', new Cache); +$registry->store('Twig', new Twig); +$registry->store('Pages', new Page\Pages); +$registry->store('Taxonomy', new Taxonomy); + +/** @var Grav $grav */ +$grav = $registry->retrieve('Grav'); +$grav->process(); + +ob_end_flush(); diff --git a/system/autoload.php b/system/autoload.php new file mode 100644 index 000000000..1e3911cef --- /dev/null +++ b/system/autoload.php @@ -0,0 +1,10 @@ +get('system.cache.prefix'); + /** @var Uri $uri */ + $uri = Registry::get('Uri'); + + $this->enabled = (bool) $config->get('system.cache.enabled'); + + // Cache key allows us to invalidate all cache on configuration changes. + $this->key = substr(md5(($prefix ? $prefix : 'g') . $uri->rootUrl(true) . $config->key . GRAV_VERSION), 2, 8); + + switch ($this->getCacheDriverName($config->get('system.cache.driver'))) { + case 'apc': + $driver = new \Doctrine\Common\Cache\ApcCache(); + break; + + case 'wincache': + $driver = new \Doctrine\Common\Cache\WinCacheCache(); + break; + + case 'xcache': + $driver = new \Doctrine\Common\Cache\XcacheCache(); + break; + + case 'memcache': + $driver = new \Doctrine\Common\Cache\MemcacheCache(); + break; + + case 'memcached': + $driver = new \Doctrine\Common\Cache\MemcachedCache(); + break; + + default: + $driver = new \Doctrine\Common\Cache\FilesystemCache(CACHE_DIR); + break; + } + $this->driver = $driver; + } + + /** + * Automatically picks the cache mechanism to use. If you pick one manually it will use that + * If there is no config option for $driver in the config, or it's set to 'auto', it will + * pick the best option based on which cache extensions are installed. + * + * @param string $setting + * @return string The name of the best cache driver to use + */ + protected function getCacheDriverName($setting = null) + { + + if (!$setting || $setting == 'auto') { + if (extension_loaded('apc') && ini_get('apc.enabled')) { + return 'apc'; + } elseif (extension_loaded('wincache')) { + return 'wincache'; + } elseif (extension_loaded('xcache') && ini_get('xcache.size') && ini_get('xcache.cacher')) { + return 'xcache'; + } else { + return 'file'; + } + } else { + return $setting; + } + } + + /** + * Gets a cached entry if it exists based on an id. If it does not exist, it returns false + * + * @param string $id the id of the cached entry + * @return object returns the cached entry, can be any type, or false if doesn't exist + */ + public function fetch($id) + { + if ($this->enabled) { + $id = $this->key . $id; + return $this->driver->fetch($id); + } else { + return false; + } + } + + /** + * Stores a new cached entry. + * + * @param string $id the id of the cached entry + * @param array|object $data the data for the cached entry to store + * @param int $lifetime the lifetime to store the entry in seconds + */ + public function save($id, $data, $lifetime = null) + { + if ($this->enabled) { + $id = $this->key . $id; + $this->driver->save($id, $data, $lifetime); + } + } + + /** + * Getter method to get the cache key + */ + public function getKey() + { + return $this->key; + } +} diff --git a/system/src/Grav/Common/Config.php b/system/src/Grav/Common/Config.php new file mode 100644 index 000000000..0228c5db9 --- /dev/null +++ b/system/src/Grav/Common/Config.php @@ -0,0 +1,256 @@ +filename = realpath(dirname($filename)) . '/' . basename($filename); + + $this->reload(false); + } + + /** + * Force reload of the configuration from the disk. + * + * @param bool $force + * @return $this + */ + public function reload($force = true) + { + // Build file map. + $files = $this->build(); + $key = md5(serialize($files) . GRAV_VERSION); + + if ($force || $key != $this->key) { + // First take non-blocking lock to the file. + File\Config::instance($this->filename)->lock(false); + + // Reset configuration. + $this->items = array(); + $this->files = array(); + $this->init($files); + $this->key = $key; + } + + return $this; + } + + /** + * Save configuration into file. + * + * Note: Only saves the file if updated flag is set! + * + * @return $this + * @throws \RuntimeException + */ + public function save() + { + // If configuration was updated, store it as cached version. + try { + $file = File\Config::instance($this->filename); + + // Only save configuration file if it wasn't locked. Also invalidate opcache after saving. + // This prevents us from saving the file multiple times in a row and gives faster recovery. + if ($file->locked() !== false) { + $file->save($this); + $file->unlock(); + } + $this->updated = false; + } catch (\Exception $e) { + throw new \RuntimeException('Writing to cache folder failed (configuration).', 500, $e); + } + + return $this; + } + + /** + * Gets configuration instance. + * + * @param string $filename + * @return \Grav\Common\Config + */ + public static function instance($filename) + { + // Load cached version if available.. + if (file_exists($filename)) { + clearstatcache(true, $filename); + require_once $filename; + + if (class_exists('\Grav\Config')) { + $instance = new \Grav\Config($filename); + } + } + + // Or initialize new configuration object.. + if (!isset($instance)) { + $instance = new static($filename); + } + + // If configuration was updated, store it as cached version. + if ($instance->updated) { + $instance->save(); + } + + + // If not set, add manually current base url. + if (empty($instance->items['system']['base_url_absolute'])) { + $instance->items['system']['base_url_absolute'] = Registry::get('Uri')->rootUrl(true); + } + + if (empty($instance->items['system']['base_url_relative'])) { + $instance->items['system']['base_url_relative'] = Registry::get('Uri')->rootUrl(false); + } + + return $instance; + } + + /** + * Convert configuration into an array. + * + * @return array + */ + public function toArray() + { + return array('key' => $this->key, 'files' => $this->files, 'items' => $this->items); + } + + /** + * Initialize object by loading all the configuration files. + * + * @param array $files + */ + protected function init(array $files) + { + $this->updated = true; + + // Combine all configuration files into one larger lookup table (only keys matter). + $allFiles = $files['user'] + $files['plugins'] + $files['system']; + + // Then sort the files to have all parent nodes first. + // This is to make sure that child nodes override parents content. + uksort( + $allFiles, + function($a, $b) { + $diff = substr_count($a, '/') - substr_count($b, '/'); + return $diff ? $diff : strcmp($a, $b); + } + ); + + $systemBlueprints = new Blueprints(SYSTEM_DIR . 'blueprints'); + $pluginBlueprints = new Blueprints(USER_DIR); + + $items = array(); + foreach ($allFiles as $name => $dummy) { + $lookup = array( + 'system' => SYSTEM_DIR . 'config/' . $name . YAML_EXT, + 'plugins' => USER_DIR . $name . '/' . basename($name) . YAML_EXT, + 'user' => USER_DIR . 'config/' . $name . YAML_EXT, + ); + if (strpos($name, 'plugins/') === 0) { + $blueprint = $pluginBlueprints->get("{$name}/blueprints"); + } else { + $blueprint = $systemBlueprints->get($name); + } + + $data = new Data(array(), $blueprint); + foreach ($lookup as $key => $path) { + if (is_file($path)) { + $data->merge(File\Yaml::instance($path)->content()); + } + } +// $data->validate(); +// $data->filter(); + + // Find the current sub-tree location. + $current = &$items; + $parts = explode('/', $name); + foreach ($parts as $part) { + if (!isset($current[$part])) { + $current[$part] = array(); + } + $current = &$current[$part]; + } + + // Handle both updated and deleted configuration files. + $current = $data->toArray(); + } + + $this->items = $items; + $this->files = $files; + } + + /** + * Build a list of configuration files with their timestamps. Used for loading settings and caching them. + * + * @return array + * @internal + */ + protected function build() + { + // Find all plugins with default configuration options. + $plugins = array(); + $iterator = new \DirectoryIterator(PLUGINS_DIR); + + /** @var \DirectoryIterator $plugin */ + foreach ($iterator as $plugin) { + $name = $plugin->getBasename(); + $file = $plugin->getPathname() . DS . $name . YAML_EXT; + + if (!is_file($file)) { + continue; + } + + $modified = filemtime($file); + $plugins["plugins/{$name}"] = $modified; + } + + // Find all system and user configuration files. + $options = array( + 'compare' => 'Filename', + 'pattern' => '|\.yaml$|', + 'filters' => array('key' => '|\.yaml$|'), + 'key' => 'SubPathname', + 'value' => 'MTime' + ); + + $system = Folder::all(SYSTEM_DIR . 'config', $options); + $user = Folder::all(USER_DIR . 'config', $options); + + return array('system' => $system, 'plugins' => $plugins, 'user' => $user); + } +} diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php new file mode 100644 index 000000000..6339f873a --- /dev/null +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -0,0 +1,473 @@ +name = $name; + $this->blueprints = $data; + $this->context = $context; + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @example $value = $data->get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * + * @return mixed Value. + */ + public function get($name, $default = null, $separator = '.') + { + $path = explode($separator, $name); + $current = $this->blueprints; + foreach ($path as $field) { + if (is_object($current) && isset($current->{$field})) { + $current = $current->{$field}; + } elseif (is_array($current) && isset($current[$field])) { + $current = $current[$field]; + } else { + return $default; + } + } + + return $current; + } + + /** + * Sey value by using dot notation for nested arrays/objects. + * + * @example $value = $data->set('this.is.my.nested.variable', true); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + * @param string $separator Separator, defaults to '.' + */ + public function set($name, $value, $separator = '.') + { + $path = explode($separator, $name); + $current = &$this->blueprints; + foreach ($path as $field) { + if (is_object($current)) { + // Handle objects. + if (!isset($current->{$field})) { + $current->{$field} = array(); + } + $current = &$current->{$field}; + } else { + // Handle arrays and scalars. + if (!is_array($current)) { + $current = array($field => array()); + } elseif (!isset($current[$field])) { + $current[$field] = array(); + } + $current = &$current[$field]; + } + } + + $current = $value; + } + + /** + * Return all form fields. + * + * @return array + */ + public function fields() + { + if (!isset($this->fields)) { + $this->fields = isset($this->blueprints['form']['fields']) ? $this->blueprints['form']['fields'] : array(); + $this->getFields($this->fields); + } + + return $this->fields; + } + + /** + * Validate data against blueprints. + * + * @param array $data + * @throws \RuntimeException + */ + public function validate(array $data) + { + // Initialize data + $this->fields(); + + try { + $this->validateArray($data, $this->nested); + } catch (\RuntimeException $e) { + throw new \RuntimeException(sprintf('Page validation failed: %s', $e->getMessage())); + } + } + + /** + * Merge two arrays by using blueprints. + * + * @param array $data1 + * @param array $data2 + * @return array + */ + public function mergeData(array $data1, array $data2) + { + // Initialize data + $this->fields(); + return $this->mergeArrays($data1, $data2, $this->nested); + } + + /** + * Filter data by using blueprints. + * + * @param array $data + * @return array + */ + public function filter(array $data) + { + // Initialize data + $this->fields(); + return $this->filterArray($data, $this->nested); + } + + /** + * Return data fields that do not exist in blueprints. + * + * @param array $data + * @param string $prefix + * @return array + */ + public function extra(array $data, $prefix = '') + { + // Initialize data + $this->fields(); + return $this->extraArray($data, $this->nested, $prefix); + } + + /** + * @param array $data + * @param array $rules + * @throws \RuntimeException + * @internal + */ + protected function validateArray(array $data, array $rules) + { + $this->checkRequired($data, $rules); + + foreach ($data as $key => $field) { + $val = isset($rules[$key]) ? $rules[$key] : null; + $rule = is_string($val) ? $this->rules[$val] : null; + + if ($rule) { + // Item has been defined in blueprints. + Validation::validate($field, $rule); + } elseif (is_array($field) && is_array($val)) { + // Array has been defined in blueprints. + $this->validateArray($field, $val); + } elseif (isset($this->blueprints['validation']) && $this->blueprints['validation'] == 'strict') { + // Undefined/extra item. + throw new \RuntimeException(sprintf('%s is not defined in blueprints', $key)); + } + } + } + + /** + * @param array $data + * @param array $rules + * @return array + * @internal + */ + protected function filterArray(array $data, array $rules) + { + $results = array(); + foreach ($data as $key => $field) { + $val = isset($rules[$key]) ? $rules[$key] : null; + $rule = is_string($val) ? $this->rules[$val] : null; + + if ($rule) { + // Item has been defined in blueprints. + $field = Validation::filter($field, $rule); + } elseif (is_array($field) && is_array($val)) { + // Array has been defined in blueprints. + $field = $this->filterArray($field, $val); + } elseif (isset($this->blueprints['validation']) && $this->blueprints['validation'] == 'strict') { + $field = null; + } + + if (isset($field) && (!is_array($field) || !empty($field))) { + $results[$key] = $field; + } + } + + return $results; + } + + /** + * @param array $data1 + * @param array $data2 + * @param array $rules + * @return array + * @internal + */ + protected function mergeArrays(array $data1, array $data2, array $rules) + { + foreach ($data2 as $key => $field) { + $val = isset($rules[$key]) ? $rules[$key] : null; + $rule = is_string($val) ? $this->rules[$val] : null; + + if (!$rule && array_key_exists($key, $data1) && is_array($field) && is_array($val)) { + // Array has been defined in blueprints. + $data1[$key] = $this->mergeArrays($data1[$key], $field, $val); + } else { + // Otherwise just take value from the data2. + $data1[$key] = $field; + } + } + + return $data1; + } + + /** + * @param array $data + * @param array $rules + * @param string $prefix + * @return array + * @internal + */ + protected function extraArray(array $data, array $rules, $prefix) + { + $array = array(); + foreach ($data as $key => $field) { + $val = isset($rules[$key]) ? $rules[$key] : null; + $rule = is_string($val) ? $this->rules[$val] : null; + + if ($rule) { + // Item has been defined in blueprints. + } elseif (is_array($field) && is_array($val)) { + // Array has been defined in blueprints. + $array += $this->ExtraArray($field, $val, $prefix); + } else { + // Undefined/extra item. + $array[$prefix.$key] = $field; + } + } + return $array; + } + + /** + * Gets all field definitions from the blueprints. + * + * @param array $fields + * @internal + */ + protected function getFields(array &$fields) + { + // Go though all the fields in current level. + foreach ($fields as $key => &$field) { + // Set name from the array key. + $field['name'] = $key; + + if (isset($field['fields'])) { + // Recursively get all the nested fields. + $this->getFields($field['fields']); + } else { + // Add rule. + $this->rules[$key] = &$field; + $this->addProperty($key); + + foreach ($field as $name => $value) { + // Support nested blueprints. + if ($name == '@import') { + $values = (array) $value; + if (!isset($field['fields'])) { + $field['fields'] = array(); + } + foreach ($values as $bname) { + $b = $this->context->get($bname); + $field['fields'] = array_merge($field['fields'], $b->fields()); + } + } + + // Support for callable data values. + elseif (substr($name, 0, 6) == '@data-') { + $property = substr($name, 6); + if (is_array($value)) { + $func = array_shift($value); + } else { + $func = $value; + $value = array(); + } + list($o, $f) = preg_split('/::/', $func); + if (!$f && function_exists($o)) { + $data = call_user_func_array($o, $value); + } elseif ($f && method_exists($o, $f)) { + $data = call_user_func_array(array($o, $f), $value); + } + + // If function returns a value, + if (isset($data)) { + if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) { + // Combine field and @data-field together. + $field[$property] += $data; + } else { + // Or create/replace field with @data-field. + $field[$property] = $data; + } + } + } + } + + // Initialize predefined validation rule. + if (isset($field['validate']['rule'])) { + $field['validate'] += $this->getRule($field['validate']['rule']); + } + } + } + } + + /** + * Add property to the definition. + * + * @param string $path Comma separated path to the property. + * @internal + */ + protected function addProperty($path) + { + $parts = explode('.', $path); + $item = array_pop($parts); + + $nested = &$this->nested; + foreach ($parts as $part) { + if (!isset($nested[$part])) { + $nested[$part] = array(); + } + $nested = &$nested[$part]; + } + + if (!isset($nested[$item])) { + $nested[$item] = $path; + } + } + + /** + * @param $rule + * @return array + * @internal + */ + protected function getRule($rule) + { + if (isset($this->blueprints['rules'][$rule]) && is_array($this->blueprints['rules'][$rule])) { + return $this->blueprints['rules'][$rule]; + } + return array(); + } + + /** + * @param array $data + * @param array $fields + * @throws \RuntimeException + * @internal + */ + protected function checkRequired(array $data, array $fields) { + foreach ($fields as $name => $field) { + if (!is_string($field)) { + continue; + } + $field = $this->rules[$field]; + if (isset($field['validate']['required']) + && $field['validate']['required'] == true + && empty($data[$name])) { + throw new \RuntimeException("Missing required field: {$field['name']}"); + } + } + } + + /** + * Convert blueprints into an array. + * + * @return array + */ + public function toArray() + { + return $this->blueprints; + } + + /** + * Convert blueprints into YAML string. + * + * @return string + */ + public function toYaml() + { + return Yaml::dump($this->blueprints); + } + + /** + * Convert blueprints into JSON string. + * + * @return string + */ + public function toJson() + { + return json_encode($this->blueprints); + } + + /** + * Extend blueprint with another blueprint. + * + * @param Blueprint $extends + * @param bool $append + */ + public function extend(Blueprint $extends, $append = false) + { + $blueprints = $append ? $this->blueprints : $extends->toArray(); + $appended = $append ? $extends->toArray() : $this->blueprints; + + $bref_stack = array(&$blueprints); + $head_stack = array($appended); + + do { + end($bref_stack); + + $bref = &$bref_stack[key($bref_stack)]; + $head = array_pop($head_stack); + + unset($bref_stack[key($bref_stack)]); + + foreach (array_keys($head) as $key) { + if (isset($key, $bref[$key]) && is_array($bref[$key]) && is_array($head[$key])) { + $bref_stack[] = &$bref[$key]; + $head_stack[] = $head[$key]; + } else { + $bref = array_merge($bref, array($key => $head[$key])); + } + } + } while(count($head_stack)); + + $this->blueprints = $blueprints; + } +} diff --git a/system/src/Grav/Common/Data/Blueprints.php b/system/src/Grav/Common/Data/Blueprints.php new file mode 100644 index 000000000..a247741ae --- /dev/null +++ b/system/src/Grav/Common/Data/Blueprints.php @@ -0,0 +1,81 @@ +search = rtrim($search, '\\/') . '/'; + } + + /** + * Get blueprint. + * + * @param string $type Blueprint type. + * @return Blueprint + * @throws \RuntimeException + */ + public function get($type) + { + if (!isset($this->instances[$type])) { + if (is_file($this->search . $type . YAML_EXT)) { + $blueprints = (array) Yaml::parse($this->search . $type . YAML_EXT); + } else { + // throw new \RuntimeException("Blueprints for '{$type}' cannot be found! {$this->search}{$type}"); + $blueprints = array(); + } + + $blueprint = new Blueprint($type, $blueprints, $this); + + if (isset($blueprints['@extends'])) { + // Extend blueprint by other blueprints. + $extends = (array) $blueprints['@extends']; + foreach ($extends as $extendType) { + $blueprint->extend($this->get($extendType)); + } + } + + $this->instances[$type] = $blueprint; + } + + return $this->instances[$type]; + } + + /** + * Get all available blueprint types. + * + * @return array List of type=>name + */ + public function types() + { + if ($this->types === null) { + $this->types = array(); + + $iterator = new \DirectoryIterator($this->search); + /** @var \DirectoryIterator $file */ + foreach ($iterator as $file) { + if (!$file->isFile() || '.' . $file->getExtension() != YAML_EXT) { + continue; + } + $name = $file->getBasename(YAML_EXT); + $this->types[$name] = ucfirst(strtr($name, '_', ' ')); + } + } + return $this->types; + } +} diff --git a/system/src/Grav/Common/Data/Data.php b/system/src/Grav/Common/Data/Data.php new file mode 100644 index 000000000..27eef427f --- /dev/null +++ b/system/src/Grav/Common/Data/Data.php @@ -0,0 +1,235 @@ +items = $items; + + $this->blueprints = $blueprints; + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @example $value = $data->value('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function value($name, $default = null, $separator = '.') + { + return $this->get($name, $default, $separator); + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @example $value = $data->get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function get($name, $default = null, $separator = '.') + { + $path = explode($separator, $name); + $current = $this->items; + foreach ($path as $field) { + if (is_object($current) && isset($current->{$field})) { + $current = $current->{$field}; + } elseif (is_array($current) && isset($current[$field])) { + $current = $current[$field]; + } else { + return $default; + } + } + + return $current; + } + + /** + * Sey value by using dot notation for nested arrays/objects. + * + * @example $value = $data->set('this.is.my.nested.variable', true); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + * @param string $separator Separator, defaults to '.' + */ + public function set($name, $value, $separator = '.') + { + $path = explode($separator, $name); + $current = &$this->items; + foreach ($path as $field) { + if (is_object($current)) { + // Handle objects. + if (!isset($current->{$field})) { + $current->{$field} = array(); + } + $current = &$current->{$field}; + } else { + // Handle arrays and scalars. + if (!is_array($current)) { + $current = array($field => array()); + } elseif (!isset($current[$field])) { + $current[$field] = array(); + } + $current = &$current[$field]; + } + } + + $current = $value; + } + + /** + * Set default value by using dot notation for nested arrays/objects. + * + * @example $data->def('this.is.my.nested.variable', 'default'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function def($name, $default = null, $separator = '.') + { + $this->set($name, $this->get($name, $default, $separator), $separator); + } + + /** + * Merge two sets of data together. + * + * @param array $data + */ + public function merge(array $data) + { + if ($this->blueprints) { + $this->items = $this->blueprints->mergeData($this->items, $data); + } else { + $this->items = array_merge($this->items, $data); + } + } + + /** + * Return blueprints. + * + * @return Blueprint + */ + public function blueprints() + { + return $this->blueprints; + } + + /** + * Validate by blueprints. + * + * @throws \Exception + */ + public function validate() + { + if ($this->blueprints) { + $this->blueprints->validate($this->items); + } + } + + /** + * Filter all items by using blueprints. + */ + public function filter() + { + if ($this->blueprints) { + $this->items = $this->blueprints->filter($this->items); + } + } + + /** + * Get extra items which haven't been defined in blueprints. + * + * @return array + */ + public function extra() + { + return $this->blueprints ? $this->blueprints->extra($this->items) : array(); + } + + /** + * Save data if storage has been defined. + */ + public function save() + { + $file = $this->file(); + if ($file) { + $file->save($this->items); + } + } + + /** + * Returns whether the data already exists in the storage. + * + * NOTE: This method does not check if the data is current. + * + * @return bool + */ + public function exists() + { + return $this->file()->exists(); + } + + /** + * Return unmodified data as raw string. + * + * NOTE: This function only returns data which has been saved to the storage. + * + * @return string + */ + public function raw() + { + return $this->file()->raw(); + } + + /** + * Set or get the data storage. + * + * @param FileInterface $storage Optionally enter a new storage. + * @return FileInterface + */ + public function file(FileInterface $storage = null) + { + if ($storage) { + $this->storage = $storage; + } + return $this->storage; + } +} diff --git a/system/src/Grav/Common/Data/DataInterface.php b/system/src/Grav/Common/Data/DataInterface.php new file mode 100644 index 000000000..7ce6ec518 --- /dev/null +++ b/system/src/Grav/Common/Data/DataInterface.php @@ -0,0 +1,69 @@ +value('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function value($name, $default = null, $separator = '.'); + + /** + * Merge external data. + * + * @param array $data + * @return mixed + */ + public function merge(array $data); + + /** + * Return blueprints. + */ + public function blueprints(); + + /** + * Validate by blueprints. + * + * @throws \Exception + */ + public function validate(); + + /** + * Filter all items by using blueprints. + */ + public function filter(); + + /** + * Get extra items which haven't been defined in blueprints. + */ + public function extra(); + + /** + * Save data into the file. + */ + public function save(); + + /** + * Set or get the data storage. + * + * @param FileInterface $storage Optionally enter a new storage. + * @return FileInterface + */ + public function file(FileInterface $storage = null); +} diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php new file mode 100644 index 000000000..3390349f4 --- /dev/null +++ b/system/src/Grav/Common/Data/Validation.php @@ -0,0 +1,603 @@ + $params) { + $method = 'validate'.strtr($rule, '-', '_'); + if (method_exists(__CLASS__, $method)) { + $success = self::$method($value, $params); + if (!$success) { + throw new \RuntimeException('Failed'); + } + } + } + } + + /** + * Filter value against a blueprint field definition. + * + * @param mixed $value + * @param array $field + * @return mixed Filtered value. + */ + public static function filter($value, array $field) + { + $validate = isset($field['validate']) ? (array) $field['validate'] : array(); + + // If value isn't required, we will return null if empty value is given. + if (empty($validate['required']) && ($value === null || $value === '')) { + return null; + } + + // Validate type with fallback type text. + $type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type']; + $method = 'filter'.strtr($type, '-', '_'); + if (method_exists(__CLASS__, $method)) { + $value = self::$method($value, $validate, $field); + } else { + $value = self::filterText($value, $validate, $field); + } + + return $value; + } + + /** + * HTML5 input: text + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeText($value, array $params, array $field) + { + if (!is_string($value)) { + return false; + } + + if (isset($params['min']) && strlen($value) < $params['min']) { + return false; + } + + if (isset($params['max']) && strlen($value) > $params['max']) { + return false; + } + + $min = isset($params['min']) ? $params['min'] : 0; + if (isset($params['step']) && (strlen($value) - $min) % $params['step'] == 0) { + return false; + } + + if ((!isset($params['multiline']) || !$params['multiline']) && preg_match('/\R/um', $value)) { + return false; + } + + return true; + } + + protected static function filterText($value, array $params, array $field) + { + if (!is_string($value)) { + var_dump($value); + var_dump($params); + var_dump($field); + die(); + } + return (string) $value; + } + + protected static function filterCommaList($value, array $params, array $field) + { + return is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY); + } + + protected static function typeCommaList($value, array $params, array $field) + { + return is_array($value) ? true : self::typeText($value, $params, $field); + } + + /** + * HTML5 input: textarea + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeTextarea($value, array $params, array $field) + { + if (!isset($params['multiline'])) { + $params['multiline'] = true; + } + + return self::typeText($value, $params, $field); + } + + /** + * HTML5 input: password + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typePassword($value, array $params, array $field) + { + return self::typeText($value, $params, $field); + } + + /** + * HTML5 input: hidden + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeHidden($value, array $params, array $field) + { + return self::typeText($value, $params, $field); + } + + /** + * Custom input: checkbox list + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeCheckboxes($value, array $params, array $field) + { + return self::typeArray((array) $value, $params, $field); + } + + protected static function filterCheckboxes($value, array $params, array $field) + { + return self::filterArray($value, $params, $field); + } + + /** + * HTML5 input: checkbox + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeCheckbox($value, array $params, array $field) + { + $value = (string) $value; + + if (!isset($field['value'])) { + $field['value'] = 1; + } + if ($value && $value != $field['value']) { + return false; + } + + return true; + } + + /** + * HTML5 input: radio + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeRadio($value, array $params, array $field) + { + return self::typeArray((array) $value, $params, $field); + } + + /** + * Custom input: toggle + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeToggle($value, array $params, array $field) + { + return self::typeArray((array) $value, $params, $field); + } + + /** + * HTML5 input: select + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeSelect($value, array $params, array $field) + { + return self::typeArray((array) $value, $params, $field); + } + + /** + * HTML5 input: number + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + + public static function typeNumber($value, array $params, array $field) + { + if (!is_numeric($value)) { + return false; + } + + if (isset($params['min']) && $value < $params['min']) { + return false; + } + + if (isset($params['max']) && $value > $params['max']) { + return false; + } + + $min = isset($params['min']) ? $params['min'] : 0; + if (isset($params['step']) && fmod($value - $min, $params['step']) == 0) { + return false; + } + + return true; + } + + protected static function filterNumber($value, array $params, array $field) + { + return (int) $value; + } + + /** + * HTML5 input: range + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeRange($value, array $params, array $field) + { + return self::typeNumber($value, $params, $field); + } + + protected static function filterRange($value, array $params, array $field) + { + return self::filterNumber($value, $params, $field); + } + + /** + * HTML5 input: color + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeColor($value, array $params, array $field) + { + return preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value); + } + + /** + * HTML5 input: email + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeEmail($value, array $params, array $field) + { + return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_EMAIL); + } + + /** + * HTML5 input: url + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + + public static function typeUrl($value, array $params, array $field) + { + return self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_URL); + } + + /** + * HTML5 input: datetime + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeDatetime($value, array $params, array $field) + { + // TODO: add min, max and range. + if ($value instanceof \DateTime) { + return true; + } elseif (!is_string($value)) { + return false; + } elseif (!isset($params['format'])) { + return false !== strtotime($value); + } + + $dateFromFormat = \DateTime::createFromFormat($params['format'], $value); + + return $dateFromFormat && $value === date($params['format'], $dateFromFormat->getTimestamp()); + } + + /** + * HTML5 input: datetime-local + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeDatetime_local($value, array $params, array $field) + { + return self::typeDatetime($value, $params, $field); + } + + /** + * HTML5 input: date + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeDate($value, array $params, array $field) + { + $params = array($params); + if (!isset($params['format'])) { + $params['format'] = 'Y-m-d'; + } + return self::typeDatetime($value, $params, $field); + } + + /** + * HTML5 input: time + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeTime($value, array $params, array $field) + { + $params = array($params); + if (!isset($params['format'])) { + $params['format'] = 'H:i'; + } + return self::typeDatetime($value, $params, $field); + } + + /** + * HTML5 input: month + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeMonth($value, array $params, array $field) + { + $params = array($params); + if (!isset($params['format'])) { + $params['format'] = 'Y-m'; + } + return self::typeDatetime($value, $params, $field); + } + + /** + * HTML5 input: week + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeWeek($value, array $params, array $field) + { + if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) { + return false; + } + return self::typeDatetime($value, $params, $field); + } + + /** + * Custom input: array + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeArray($value, array $params, array $field) + { + if (!is_array($value)) { + return false; + } + + if (isset($field['multiple'])) { + if (isset($params['min']) && count($value) < $params['min']) { + return false; + } + + if (isset($params['max']) && count($value) > $params['max']) { + return false; + } + + $min = isset($params['min']) ? $params['min'] : 0; + if (isset($params['step']) && (count($value) - $min) % $params['step'] == 0) { + return false; + } + } + + $options = isset($field['options']) ? array_keys($field['options']) : array(); + $values = isset($field['use']) && $field['use'] == 'keys' ? array_keys($value) : $value; + if ($options && array_diff($values, $options)) { + return false; + } + + return true; + } + + protected static function filterArray($value, $params, $field) + { + $values = (array) $value; + $options = isset($field['options']) ? array_keys($field['options']) : array(); + + if ($options) { + $useKey = isset($field['use']) && $field['use'] == 'keys'; + foreach ($values as $key => $value) { + $values[$key] = $useKey ? (bool) $value : $value; + } + } + + return $values; + } + + /** + * Custom input: ignore (will not validate) + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeIgnore($value, array $params, array $field) + { + return true; + } + + // HTML5 attributes (min, max and range are handled inside the types) + + public static function validateRequired($value, $params) + { + return (bool) $params != true || !empty($value); + } + + public static function validatePattern($value, $params) + { + return (bool) preg_match("`^{$params}$`u", $value); + } + + + // Internal types + + public static function validateAlpha($value, $params) + { + return ctype_alpha($value); + } + + public static function validateAlnum($value, $params) + { + return ctype_alnum($value); + } + + public static function typeBool($value, $params) + { + return is_bool($value) || $value == 1 || $value == 0; + } + + public static function validateBool($value, $params) + { + return is_bool($value) || $value == 1 || $value == 0; + } + + protected static function filterBool($value, $params) + { + return (bool) $value; + } + + public static function validateDigit($value, $params) + { + return ctype_digit($value); + } + + public static function validateFloat($value, $params) + { + return is_float(filter_var($value, FILTER_VALIDATE_FLOAT)); + } + + protected static function filterFloat($value, $params) + { + return (float) $value; + } + + public static function validateHex($value, $params) + { + return ctype_xdigit($value); + } + + public static function validateInt($value, $params) + { + return is_numeric($value) && (int) $value == $value; + } + + protected static function filterInt($value, $params) + { + return (int) $value; + } + + public static function validateArray($value, $params) + { + return is_array($value) || ($value instanceof \ArrayAccess + && $value instanceof \Traversable + && $value instanceof \Countable); + } + + public static function validateJson($value, $params) + { + return (bool) (json_decode($value)); + } +} diff --git a/system/src/Grav/Common/Filesystem/File/Config.php b/system/src/Grav/Common/Filesystem/File/Config.php new file mode 100644 index 000000000..d697b1f65 --- /dev/null +++ b/system/src/Grav/Common/Filesystem/File/Config.php @@ -0,0 +1,129 @@ +filename); + } elseif (function_exists('apc_invalidate')) { + // APC + @apc_invalidate($this->filename); + } + } + + /** + * Check contents and make sure it is in correct format. + * + * @param \Grav\Common\Config $var + * @return \Grav\Common\Config + * @throws \RuntimeException + */ + protected function check($var) + { + if (!($var instanceof \Grav\Common\Config)) { + throw new \RuntimeException('Provided data is not configuration'); + } + + return $var; + } + + /** + * Encode configuration object into RAW string (PHP class). + * + * @param \Grav\Common\Config $var + * @return string + * @throws \RuntimeException + */ + protected function encode($var) + { + if (!($var instanceof \Grav\Common\Config)) { + throw new \RuntimeException('Provided data is not configuration'); + } + + // Build the object variables string + $vars = array(); + $options = $var->toArray(); + + foreach ($options as $k => $v) { + if (is_int($v)) { + $vars[] = "\tpublic $" . $k . " = " . $v . ";"; + } elseif (is_bool($v)) { + $vars[] = "\tpublic $" . $k . " = " . ($v ? 'true' : 'false') . ";"; + } elseif (is_scalar($v)) { + $vars[] = "\tpublic $" . $k . " = '" . addcslashes($v, '\\\'') . "';"; + } elseif (is_array($v) || is_object($v)) { + $vars[] = "\tpublic $" . $k . " = " . $this->encodeArray((array) $v) . ";"; + } + } + $vars = implode("\n", $vars); + + return " $v) { + if (is_array($v) || is_object($v)) { + $r[] = '"' . $k . '" => ' . $this->encodeArray((array) $v, $level+1); + } elseif (is_int($v)) { + $r[] = "'" . $k . "' => " . $v; + } elseif (is_bool($v)) { + $r[] = "'" . $k . "' => " . ($v ? 'true' : 'false'); + } else { + $r[] .= "'" . $k . "' => " . "'" . addslashes($v) . "'"; + } + } + + $tabs = str_repeat("\t", $level); + return "array(\n\t{$tabs}" . implode(",\n\t{$tabs}", $r) . "\n{$tabs})"; + } + + /** + * Decode RAW string into contents. + * + * @param string $var + * @return \Grav\Common\Config + */ + protected function decode($var) + { + // TODO: improve this one later, works only for single file... + return class_exists('\Grav\Config') ? new \Grav\Config($this->filename) : new Config($this->filename); + } +} diff --git a/system/src/Grav/Common/Filesystem/File/General.php b/system/src/Grav/Common/Filesystem/File/General.php new file mode 100644 index 000000000..d9530f499 --- /dev/null +++ b/system/src/Grav/Common/Filesystem/File/General.php @@ -0,0 +1,352 @@ +init($filename); + } + return static::$instances[$filename]; + } + + /** + * Prevent constructor from being used. + * + * @internal + */ + protected function __construct() + { + } + + /** + * Prevent cloning. + * + * @internal + */ + protected function __clone() + { + //Me not like clones! Me smash clones! + } + + /** + * Set filename. + * + * @param $filename + */ + protected function init($filename) + { + $this->filename = $filename; + } + + /** + * Get/set the file location. + * + * @param string $var + * @return string + */ + public function filename($var = null) + { + if ($var !== null) { + $this->filename = $var; + } + return $this->filename; + } + + /** + * Return basename of the file. + * + * @return string + */ + public function basename() + { + return basename($this->filename, $this->extension); + } + + /** + * Check if file exits. + * + * @return bool + */ + public function exists() + { + return is_file($this->filename); + } + + /** + * Return file modification time. + * + * @return int|bool Timestamp or false if file doesn't exist. + */ + public function modified() + { + return is_file($this->filename) ? filemtime($this->filename) : false; + } + + /** + * Lock file for writing. You need to manually unlock(). + * + * @param bool $block For non-blocking lock, set the parameter to false. + * @return bool + */ + public function lock($block = true) + { + if (!$this->handle) { + $this->handle = fopen($this->filename, 'wb+'); + } + $lock = $block ? LOCK_EX : LOCK_EX | LOCK_NB; + return $this->locked = flock($this->handle, $lock); + } + + /** + * Returns true if file has been locked for writing. + * + * @return bool|null True = locked, false = failed, null = not locked. + */ + public function locked() + { + return $this->locked; + } + + /** + * Unlock file. + * + * @return bool + */ + public function unlock() + { + if (!$this->handle) { + return; + } + if ($this->locked) { + flock($this->handle, LOCK_UN); + $this->locked = null; + } + fclose($this->handle); + } + + /** + * Check if file can be written. + * + * @return bool + */ + public function writable() + { + return is_writable($this->filename) || $this->writableDir(dirname($this->filename)); + } + + /** + * (Re)Load a file and return RAW file contents. + * + * @return string + */ + public function load() + { + $this->raw = $this->exists() ? (string) file_get_contents($this->filename) : ''; + $this->content = null; + + return $this->raw; + } + + /** + * Get/set raw file contents. + * + * @param string $var + * @return string + */ + public function raw($var = null) + { + if ($var !== null) { + $this->raw = (string) $var; + $this->content = null; + } + + if (!is_string($this->raw)) { + $this->raw = $this->load(); + } + + return $this->raw; + } + + /** + * Get/set parsed file contents. + * + * @param mixed $var + * @return string + */ + public function content($var = null) + { + if ($var !== null) { + $this->content = $this->check($var); + + // Update RAW, too. + $this->raw = $this->encode($this->content); + + } elseif ($this->content === null) { + // Decode RAW file. + $this->content = $this->decode($this->raw()); + } + + return $this->content; + } + + /** + * Save file. + * + * @param mixed $data Optional data to be saved, usually array. + * @throws \RuntimeException + */ + public function save($data = null) + { + if ($data !== null) { + $this->content($data); + } + + if (!$this->mkdir(dirname($this->filename))) { + throw new \RuntimeException('Creating directory failed for ' . $this->filename); + } + if (!$this->locked) { + // Obtain blocking lock or fail. + if (!$this->lock()) { + throw new \RuntimeException('Obtaining write lock failed on file: ' . $this->filename); + } + $lock = true; + } + + if (@fwrite($this->handle, $this->raw()) === false) { + $this->unlock(); + throw new \RuntimeException('Saving file failed: ' . $this->filename); + } + + if (isset($lock)) { + $this->unlock(); + } + + // Touch the directory as well, thus marking it modified. + @touch(dirname($this->filename)); + } + + /** + * Delete file from filesystem. + * + * @return bool + */ + public function delete() + { + return unlink($this->filename); + } + + /** + * Check contents and make sure it is in correct format. + * + * Override in derived class. + * + * @param string $var + * @return string + */ + protected function check($var) + { + return (string) $var; + } + + /** + * Encode contents into RAW string. + * + * Override in derived class. + * + * @param string $var + * @return string + */ + protected function encode($var) + { + return (string) $var; + } + + /** + * Decode RAW string into contents. + * + * Override in derived class. + * + * @param string $var + * @return string mixed + */ + protected function decode($var) + { + return (string) $var; + } + + /** + * @param string $dir + * @return bool + * @internal + */ + protected function mkdir($dir) + { + return is_dir($dir) || mkdir($dir, 0777, true); + } + + /** + * @param string $dir + * @return bool + * @internal + */ + protected function writableDir($dir) + { + if ($dir && !file_exists($dir)) { + return $this->writableDir(dirname($dir)); + } + + return $dir && is_dir($dir) && is_writable($dir); + } +} diff --git a/system/src/Grav/Common/Filesystem/File/Json.php b/system/src/Grav/Common/Filesystem/File/Json.php new file mode 100644 index 000000000..b7ea18e5d --- /dev/null +++ b/system/src/Grav/Common/Filesystem/File/Json.php @@ -0,0 +1,54 @@ +extension = '.log'; + } + + /** + * Check contents and make sure it is in correct format. + * + * @param array $var + * @return array + */ + protected function check($var) + { + return (array) $var; + } + + /** + * Encode contents into RAW string (unsupported). + * + * @param string $var + * @throws \Exception + */ + protected function encode($var) + { + throw new \Exception('Saving log file is forbidden.'); + } + + /** + * Decode RAW string into contents. + * + * @param string $var + * @return array mixed + */ + protected function decode($var) + { + $lines = (array) preg_split('#(\r\n|\n|\r)#', $var); + + $results = array(); + foreach ($lines as $line) { + preg_match('#^\[(.*)\] (.*) @ (.*) @@ (.*)$#', $line, $matches); + if ($matches) { + $results[] = ['date' => $matches[1], 'message' => $matches[2], 'url' => $matches[3], 'file' => $matches[4]]; + } + } + + return $results; + } +} diff --git a/system/src/Grav/Common/Filesystem/File/Markdown.php b/system/src/Grav/Common/Filesystem/File/Markdown.php new file mode 100644 index 000000000..8fe615dae --- /dev/null +++ b/system/src/Grav/Common/Filesystem/File/Markdown.php @@ -0,0 +1,120 @@ +content(); + + if ($var !== null) { + $content['header'] = $var; + $this->content($content); + } + + return $content['header']; + } + + /** + * Get/set markdown content. + * + * @param string $var + * + * @return string + */ + public function markdown($var = null) + { + $content = $this->content(); + + if ($var !== null) { + $content['markdown'] = (string) $var; + $this->content($content); + } + + return $content['markdown']; + } + + /** + * Check contents and make sure it is in correct format. + * + * @param array $var + * @return array + */ + protected function check($var) + { + $var = (array) $var; + if (!isset($var['header']) || !is_array($var['header'])) { + $var['header'] = array(); + } + if (!isset($var['markdown']) || !is_string($var['markdown'])) { + $var['markdown'] = ''; + } + + return $var; + } + + /** + * Encode contents into RAW string. + * + * @param string $var + * @return string + */ + protected function encode($var) + { + // Create Markdown file with YAML header. + $o = (!empty($var['header']) ? "---\n" . trim(YamlParser::dump($var['header'])) . "\n---\n\n" : '') . $var['markdown']; + + // Normalize line endings to Unix style. + $o = preg_replace("/(\r\n|\r)/", "\n", $o); + + return $o; + } + + /** + * Decode RAW string into contents. + * + * @param string $var + * @return array mixed + */ + protected function decode($var) + { + $content = array(); + + // Normalize line endings to Unix style. + $var = preg_replace("/(\r\n|\r)/", "\n", $var); + + // Parse header. + preg_match("/---\n(.+?)\n---(\n\n|$)/uism", $this->raw(), $m); + $content['header'] = isset($m[1]) ? YamlParser::parse(preg_replace("/\n\t/", "\n ", $m[1])) : array(); + + // Strip header to get content. + $content['markdown'] = trim(preg_replace("/---\n(.+?)\n---(\n\n|$)/uism", '', $var)); + + return $content; + } +} diff --git a/system/src/Grav/Common/Filesystem/File/Yaml.php b/system/src/Grav/Common/Filesystem/File/Yaml.php new file mode 100644 index 000000000..e93e0e03e --- /dev/null +++ b/system/src/Grav/Common/Filesystem/File/Yaml.php @@ -0,0 +1,61 @@ +extension = YAML_EXT; + } + + /** + * Check contents and make sure it is in correct format. + * + * @param array $var + * @return array + */ + protected function check($var) + { + return (array) $var; + } + + /** + * Encode contents into RAW string. + * + * @param string $var + * @return string + */ + protected function encode($var) + { + return (string) YamlParser::dump($var); + } + + /** + * Decode RAW string into contents. + * + * @param string $var + * @return array mixed + */ + protected function decode($var) + { + return (array) YamlParser::parse($var); + } +} diff --git a/system/src/Grav/Common/Filesystem/FileInterface.php b/system/src/Grav/Common/Filesystem/FileInterface.php new file mode 100644 index 000000000..a090e550c --- /dev/null +++ b/system/src/Grav/Common/Filesystem/FileInterface.php @@ -0,0 +1,100 @@ +getMTime(); + if ($dir_modified > $last_modified) { + $last_modified = $dir_modified; + } + } + return $last_modified; + } + + /** + * Return recursive list of all files and directories under given path. + * + * @param string $path + * @param array $params + * @return array + * @throws \RuntimeException + */ + public static function all($path, array $params = array()) + { + $path = realpath($path); + + if ($path === false) { + throw new \RuntimeException("Path to {$path} doesn't exist."); + } + + $compare = $params['compare'] ? 'get' . $params['compare'] : null; + $pattern = $params['pattern'] ? $params['pattern'] : null; + $filters = $params['filters'] ? $params['filters'] : null; + $key = $params['key'] ? 'get' . $params['key'] : null; + $value = $params['value'] ? 'get' . $params['value'] : 'SubPathname'; + + $directory = new \RecursiveDirectoryIterator($path, + \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS + \FilesystemIterator::CURRENT_AS_SELF); + $iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST); + + $results = array(); + + /** @var \RecursiveDirectoryIterator $file */ + foreach ($iterator as $file) { + if ($compare && $pattern && !preg_match($pattern, $file->{$compare}())) { + continue; + } + $fileKey = $key ? $file->{$key}() : null; + $filePath = $file->{$value}(); + if ($filters) { + if (isset($filters['key'])) { + $fileKey = preg_replace($filters['key'], '', $fileKey); + } + if (isset($filters['value'])) { + $filePath = preg_replace($filters['value'], '', $filePath); + } + } + + $results[$fileKey] = $filePath; + } + return $results; + } + + /** + * Recursively copy directory in filesystem. + * + * @param string $source + * @param string $target + * @throws \RuntimeException + */ + public static function copy($source, $target) + { + $source = rtrim($source, '\\/'); + $target = rtrim($target, '\\/'); + + if (!is_dir($source)) { + throw new \RuntimeException('Cannot copy non-existing folder.'); + } + + // Make sure that path to the target exists before copying. + self::mkdir($target); + + $success = true; + + // Go through all sub-directories and copy everything. + $files = self::all($source); + foreach ($files as $file) { + $src = $source .'/'. $file; + $dst = $target .'/'. $file; + + if (is_dir($src)) { + // Create current directory. + $success &= @mkdir($dst); + } else { + // Or copy current file. + $success &= @copy($src, $dst); + } + } + + if (!$success) { + $error = error_get_last(); + throw new \RuntimeException($error['message']); + } + + // Make sure that the change will be detected when caching. + @touch(dirname($target)); + } + + /** + * Move directory in filesystem. + * + * @param string $source + * @param string $target + * @throws \RuntimeException + */ + public static function move($source, $target) + { + if (!is_dir($source)) { + throw new \RuntimeException('Cannot move non-existing folder.'); + } + + // Make sure that path to the target exists before moving. + self::mkdir(dirname($target)); + + // Just rename the directory. + $success = @rename($source, $target); + + if (!$success) { + $error = error_get_last(); + throw new \RuntimeException($error['message']); + } + + // Make sure that the change will be detected when caching. + @touch(dirname($source)); + @touch(dirname($target)); + } + + /** + * Recursively delete directory from filesystem. + * + * @param string $target + * @throws \RuntimeException + */ + public static function delete($target) + { + if (!is_dir($target)) { + throw new \RuntimeException('Cannot delete non-existing folder.'); + } + + $success = self::doDelete($target); + + if (!$success) { + $error = error_get_last(); + throw new \RuntimeException($error['message']); + } + + // Make sure that the change will be detected when caching. + @touch(dirname($target)); + } + + /** + * @param string $folder + * @return bool + * @internal + */ + protected static function doDelete($folder) + { + // Special case for symbolic links. + if (is_link($folder)) { + return @unlink($folder); + } + + // Go through all items in filesystem and recursively remove everything. + $files = array_diff(scandir($folder), array('.', '..')); + foreach ($files as $file) { + $path = "{$folder}/{$file}"; + (is_dir($path)) ? self::doDelete($path) : @unlink($path); + } + + return @rmdir($folder); + } + + /** + * @param string $folder + * @throws \RuntimeException + * @internal + */ + protected static function mkdir($folder) + { + if (is_dir($folder)) { + return; + } + + $success = @mkdir($folder, 0777, true); + + if (!$success) { + $error = error_get_last(); + throw new \RuntimeException($error['message']); + } + } +} diff --git a/system/src/Grav/Common/Getters.php b/system/src/Grav/Common/Getters.php new file mode 100644 index 000000000..699f9153b --- /dev/null +++ b/system/src/Grav/Common/Getters.php @@ -0,0 +1,150 @@ +offsetSet($offset, $value); + } + + /** + * Magic getter method + * + * @param mixed $offset Asset name value + * @return mixed Asset value + */ + public function __get($offset) + { + return $this->offsetGet($offset); + } + + /** + * Magic method to determine if the attribute is set + * + * @param mixed $offset Asset name value + * @return boolean True if the value is set + */ + public function __isset($offset) + { + return $this->offsetExists($offset); + } + + /** + * Magic method to unset the attribute + * + * @param mixed $offset The name value to unset + */ + public function __unset($offset) + { + $this->offsetUnset($offset); + } + + /** + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + return isset($this->{$var}[$offset]); + } else { + return isset($this->{$offset}); + } + } + + /** + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + return isset($this->{$var}[$offset]) ? $this->{$var}[$offset] : null; + } else { + return isset($this->{$offset}) ? $this->{$offset} : null; + } + } + + /** + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + $this->{$var}[$offset] = $value; + } else { + $this->{$offset} = $value; + } + } + + /** + * @param mixed $offset + */ + public function offsetUnset($offset) + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + unset($this->{$var}[$offset]); + } else { + unset($this->{$offset}); + } + } + + /** + * @return int + */ + public function count() + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + count($this->{$var}); + } else { + count($this->toArray()); + } + } + + /** + * Returns an associative array of object properties. + * + * @return array + */ + public function toArray() + { + if ($this->gettersVariable) { + $var = $this->gettersVariable; + return $this->{$var}; + } else { + $properties = (array) $this; + $list = array(); + foreach ($properties as $property => $value) { + if ($property[0] != "\0") $list[$property] = $value; + } + return $list; + } + } +} diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php new file mode 100644 index 000000000..49c42f070 --- /dev/null +++ b/system/src/Grav/Common/Grav.php @@ -0,0 +1,214 @@ +uri = Registry::get('Uri'); + + // Get the Configuration settings and caching + $this->config = Registry::get('Config'); + + Debugger::$logDirectory = $this->config->get('system.debugger.log.enabled') ? LOG_DIR : null; + Debugger::$maxDepth = $this->config->get('system.debugger.max_depth'); + + // Switch debugger into development mode if configured + if ($this->config->get('system.debugger.enabled')) { + if (function_exists('ini_set')) { + ini_set('display_errors', true); + } + Debugger::$productionMode = Debugger::DEVELOPMENT; + $this->fireEvent('onAfterInitDebug'); + } + + // Get the Caching setup + $this->cache = Registry::get('Cache'); + $this->cache->init(); + + // Get Plugins + $plugins = new Plugins(); + $this->plugins = $plugins->load(); + $this->fireEvent('onAfterInitPlugins'); + + // Get current theme and hook it into plugins. + $themes = new Themes(); + $this->plugins['Theme'] = $themes->load(); + + // Get twig object + $this->twig = Registry::get('Twig'); + $this->twig->init(); + + // Get all the Pages that Grav knows about + $this->pages = Registry::get('Pages'); + $this->pages->init(); + $this->fireEvent('onAfterGetPages'); + + // Get the taxonomy and set it on the grav object + $this->taxonomy = Registry::get('Taxonomy'); + + // Get current page + $this->page = $this->pages->dispatch($this->uri->route()); + $this->fireEvent('onAfterGetPage'); + + // If there's no page, throw exception + if (!$this->page) { + throw new \RuntimeException('Page Not Found', 404); + } + + // Process whole page as required + $this->output = $this->twig->processSite($this->uri->extension()); + $this->fireEvent('onAfterGetOutput'); + + // Set the header type + $this->header(); + + echo $this->output; + } + + /** + * Redirect browser to another location. + * + * @param string $route Internal route. + * @param int $code Redirection code (30x) + */ + public function redirect($route, $code = 303) + { + header("Location: " . rtrim($this->uri->rootUrl(), '/') .'/'. trim($route, '/'), true, $code); + exit(); + } + + /** + * Returns mime type for the file format. + * + * @param string $format + * @return string + */ + public function mime($format) + { + switch ($format) { + case 'json': + return 'application/json'; + case 'html': + return 'text/html'; + case 'atom': + return 'application/atom+xml'; + case 'rss': + return 'application/rss+xml'; + case 'xml': + return 'application/xml'; + } + return 'text/html'; + } + + /** + * Set response header. + */ + public function header() + { + header('Content-type: ' . $this->mime($this->uri->extension())); + } + + /** + * Log a message. + * + * @param string $message + */ + protected static function log($message) + { + if (Debugger::$logDirectory) { + Debugger::log(sprintf($message, Debugger::timer() * 1000)); + } + } + + /** + * Processes any hooks and runs them. + */ + public function fireEvent() + { + $args = func_get_args(); + $hook_id = array_shift($args); + $no_timing_hooks = array('onAfterPageProcessed','onAfterFolderProcessed', 'onAfterCollectionProcessed'); + + if (!empty($this->plugins)) { + foreach ($this->plugins as $plugin) { + if (is_callable(array($plugin, $hook_id))) { + call_user_func_array(array($plugin, $hook_id), $args); + } + } + } + + if ($this->config->get('system.debugger.log.timing') && !in_array($hook_id, $no_timing_hooks)) { + static::log($hook_id.': %f ms'); + } + } +} diff --git a/system/src/Grav/Common/Inflector.php b/system/src/Grav/Common/Inflector.php new file mode 100644 index 000000000..eebbdf03c --- /dev/null +++ b/system/src/Grav/Common/Inflector.php @@ -0,0 +1,370 @@ + '\1zes', + '/^(ox)$/i' => '\1en', + '/([m|l])ouse$/i' => '\1ice', + '/(matr|vert|ind)ix|ex$/i' => '\1ices', + '/(x|ch|ss|sh)$/i' => '\1es', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(hive)$/i' => '\1s', + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', + '/sis$/i' => 'ses', + '/([ti])um$/i' => '\1a', + '/(buffal|tomat)o$/i' => '\1oes', + '/(bu)s$/i' => '\1ses', + '/(alias|status)/i'=> '\1es', + '/(octop|vir)us$/i'=> '\1i', + '/(ax|test)is$/i'=> '\1es', + '/s$/i'=> 's', + '/$/'=> 's'); + + $uncountable = array('equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep'); + + $irregular = array( + 'person' => 'people', + 'man' => 'men', + 'child' => 'children', + 'sex' => 'sexes', + 'move' => 'moves'); + + $lowercased_word = strtolower($word); + + foreach ($uncountable as $_uncountable) { + if (substr($lowercased_word, (-1*strlen($_uncountable))) == $_uncountable) { + return $word; + } + } + + foreach ($irregular as $_plural => $_singular) { + if (preg_match('/('.$_plural.')$/i', $word, $arr)) { + return preg_replace('/('.$_plural.')$/i', substr($arr[0], 0, 1).substr($_singular, 1), $word); + } + } + + foreach ($plural as $rule => $replacement) { + if (preg_match($rule, $word)) { + return preg_replace($rule, $replacement, $word); + } + } + return false; + + } + + /** + * Singularizes English nouns. + * + * @access static public + * @static + * @param string $word English noun to singularize + * @return string Singular noun. + */ + public static function singularize($word, $count = 1) + { + if ($count != 1) { + return $word; + } + + $singular = array ( + '/(quiz)zes$/i' => '\1', + '/(matr)ices$/i' => '\1ix', + '/(vert|ind)ices$/i' => '\1ex', + '/^(ox)en/i' => '\1', + '/(alias|status)es$/i' => '\1', + '/([octop|vir])i$/i' => '\1us', + '/(cris|ax|test)es$/i' => '\1is', + '/(shoe)s$/i' => '\1', + '/(o)es$/i' => '\1', + '/(bus)es$/i' => '\1', + '/([m|l])ice$/i' => '\1ouse', + '/(x|ch|ss|sh)es$/i' => '\1', + '/(m)ovies$/i' => '\1ovie', + '/(s)eries$/i' => '\1eries', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/([lr])ves$/i' => '\1f', + '/(tive)s$/i' => '\1', + '/(hive)s$/i' => '\1', + '/([^f])ves$/i' => '\1fe', + '/(^analy)ses$/i' => '\1sis', + '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', + '/([ti])a$/i' => '\1um', + '/(n)ews$/i' => '\1ews', + '/s$/i' => '', + ); + + $uncountable = array('equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep'); + + $irregular = array( + 'person' => 'people', + 'man' => 'men', + 'child' => 'children', + 'sex' => 'sexes', + 'move' => 'moves'); + + $lowercased_word = strtolower($word); + foreach ($uncountable as $_uncountable) { + if (substr($lowercased_word, (-1*strlen($_uncountable))) == $_uncountable) { + return $word; + } + } + + foreach ($irregular as $_plural => $_singular) { + if (preg_match('/('.$_singular.')$/i', $word, $arr)) { + return preg_replace('/('.$_singular.')$/i', substr($arr[0], 0, 1).substr($_plural, 1), $word); + } + } + + foreach ($singular as $rule => $replacement) { + if (preg_match($rule, $word)) { + return preg_replace($rule, $replacement, $word); + } + } + + return $word; + } + + /** + * Converts an underscored or CamelCase word into a English + * sentence. + * + * The titleize static public function converts text like "WelcomePage", + * "welcome_page" or "welcome page" to this "Welcome + * Page". + * If second parameter is set to 'first' it will only + * capitalize the first character of the title. + * + * @access static public + * @static + * @param string $word Word to format as tile + * @param string $uppercase If set to 'first' it will only uppercase the + * first character. Otherwise it will uppercase all + * the words in the title. + * @return string Text formatted as title + */ + public static function titleize($word, $uppercase = '') + { + $uppercase = $uppercase == 'first' ? 'ucfirst' : 'ucwords'; + return $uppercase(static::humanize(static::underscorize($word))); + } + + /** + * Returns given word as CamelCased + * + * Converts a word like "send_email" to "SendEmail". It + * will remove non alphanumeric character from the word, so + * "who's online" will be converted to "WhoSOnline" + * + * @access static public + * @static + * @see variablize + * @param string $word Word to convert to camel case + * @return string UpperCamelCasedWord + */ + public static function camelize($word) + { + return str_replace(' ', '', ucwords(preg_replace('/[^A-Z^a-z^0-9]+/', ' ', $word))); + } + + /** + * Converts a word "into_it_s_underscored_version" + * + * Convert any "CamelCased" or "ordinary Word" into an + * "underscored_word". + * + * This can be really useful for creating friendly URLs. + * + * @access static public + * @static + * @param string $word Word to underscore + * @return string Underscored word + */ + public static function underscorize($word) + { + $regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1_\2', $word); + $regex2 = preg_replace('/([a-zd])([A-Z])/', '\1_\2', $regex1); + $regex3 = preg_replace('/[^A-Z^a-z^0-9]+/', '_', $regex2); + return strtolower($regex3); + } + + /** + * Converts a word "into-it-s-hyphenated-version" + * + * Convert any "CamelCased" or "ordinary Word" into an + * "hyphenated-word". + * + * This can be really useful for creating friendly URLs. + * + * @access static public + * @static + * @param string $word Word to hyphenate + * @return string hyphenized word + */ + public static function hyphenize($word) + { + $regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1-\2', $word); + $regex2 = preg_replace('/([a-zd])([A-Z])/', '\1-\2', $regex1); + $regex3 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex2); + return strtolower($regex3); + } + + /** + * Returns a human-readable string from $word + * + * Returns a human-readable string from $word, by replacing + * underscores with a space, and by upper-casing the initial + * character by default. + * + * If you need to uppercase all the words you just have to + * pass 'all' as a second parameter. + * + * @access static public + * @static + * @param string $word String to "humanize" + * @param string $uppercase If set to 'all' it will uppercase all the words + * instead of just the first one. + * @return string Human-readable word + */ + public static function humanize($word, $uppercase = '') + { + $uppercase = $uppercase == 'all' ? 'ucwords' : 'ucfirst'; + return $uppercase(str_replace('_', ' ', preg_replace('/_id$/', '', $word))); + } + + /** + * Same as camelize but first char is underscored + * + * Converts a word like "send_email" to "sendEmail". It + * will remove non alphanumeric character from the word, so + * "who's online" will be converted to "whoSOnline" + * + * @access static public + * @static + * @see camelize + * @param string $word Word to lowerCamelCase + * @return string Returns a lowerCamelCasedWord + */ + public static function variablize($word) + { + $word = static::camelize($word); + return strtolower($word[0]).substr($word, 1); + } + + /** + * Converts a class name to its table name according to rails + * naming conventions. + * + * Converts "Person" to "people" + * + * @access static public + * @static + * @see classify + * @param string $class_name Class name for getting related table_name. + * @return string plural_table_name + */ + public static function tableize($class_name) + { + return static::pluralize(static::underscore($class_name)); + } + + /** + * Converts a table name to its class name according to rails + * naming conventions. + * + * Converts "people" to "Person" + * + * @access static public + * @static + * @see tableize + * @param string $table_name Table name for getting related ClassName. + * @return string SingularClassName + */ + public static function classify($table_name) + { + return static::camelize(static::singularize($table_name)); + } + + /** + * Converts number to its ordinal English form. + * + * This method converts 13 to 13th, 2 to 2nd ... + * + * @access static public + * @static + * @param integer $number Number to get its ordinal value + * @return string Ordinal representation of given string. + */ + public static function ordinalize($number) + { + if (in_array(($number % 100), range(11, 13))) { + return $number.'th'; + } else { + switch (($number % 10)) { + case 1: + return $number.'st'; + break; + case 2: + return $number.'nd'; + break; + case 3: + return $number.'rd'; + break; + default: + return $number.'th'; + break; + } + } + } + + public static function monthize($days) + { + $now = new JDate(); + $end = new JDate(); + + $duration = new DateInterval("P{$days}D"); + + $diff = $end->add($duration)->diff($now); + + // handle years + if ($diff->y > 0) { + $diff->m = $diff->m + 12*$diff->y; + } + + return $diff->m; + } +} diff --git a/system/src/Grav/Common/Iterator.php b/system/src/Grav/Common/Iterator.php new file mode 100644 index 000000000..dffa5ab78 --- /dev/null +++ b/system/src/Grav/Common/Iterator.php @@ -0,0 +1,394 @@ +items = $items; + } + + /** + * Convert function calls for the existing keys into their values. + * + * @param string $key + * @param mixed $args + * @return mixed + */ + public function __call($key, $args) + { + return (isset($this->items[$key])) ? $this->items[$key] : null; + } + + /** + * Array getter shorthand to get items. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + return (isset($this->items[$key])) ? $this->items[$key] : null; + } + + /** + * Array setter shorthand to set the value. + * + * @param string $key + * @param mixed $value + */ + public function __set($key, $value) + { + $this->items[$key] = $value; + } + + /** + * Array isset shorthand to set the value. + * + * @param string $key + * @return bool + */ + public function __isset($key) + { + return isset($this->items[$key]); + } + + /** + * Array unset shorthand to remove the key. + * + * @param string $key + */ + public function __unset($key) + { + $this->offsetUnset($key); + } + + /** + * Clone the iterator. + */ + public function __clone() + { + foreach ($this as $key => $value) { + if (is_object($value)) { + $this->$key = clone $this->$key; + } + } + } + + /** + * Convents iterator to a comma separated list. + * + * @return string + * @todo Add support to nested sets. + */ + public function __toString() + { + return implode(',', $this->items); + } + + /** + * Remove item from the list. + * + * @param $key + */ + public function remove($key) + { + $this->offsetUnset($key); + } + + /** + * Return previous item. + * + * @return mixed + */ + public function prev() + { + return prev($this->items); + } + + /** + * Return nth item. + * + * @param int $key + * @return mixed|bool + */ + public function nth($key) + { + $items = array_values($this->items); + return (isset($items[$key])) ? $this->offsetGet($items[$key]) : false; + } + + /** + * @param mixed $needle Searched value. + * @return string|bool Key if found, otherwise false. + */ + public function indexOf($needle) + { + foreach (array_values($this->items) as $key => $value) { + if ($value === $needle) { + return $key; + } + } + return false; + } + + /** + * Shuffle items. + * + * @return $this + */ + public function shuffle() + { + $keys = array_keys($this->items); + shuffle($keys); + + $new = array(); + foreach($keys as $key) { + $new[$key] = $this->items[$key]; + } + + $this->items = $new; + + return $this; + } + + /** + * Slice the list. + * + * @param int $offset + * @param int $length + * @return $this + */ + public function slice($offset, $length = null) + { + $this->items = array_slice($this->items, $offset, $length); + + return $this; + } + + /** + * Pick one or more random entries. + * + * @param int $num Specifies how many entries should be picked. + * @return $this + */ + public function random($num = 1) + { + $this->items = array_intersect_key($this->items, array_flip((array) array_rand($this->items, $num))); + + return $this; + } + + /** + * Append new elements to the list. + * + * @param array|Iterator $items Items to be appended. Existing keys will be overridden with the new values. + * @return $this + */ + public function append($items) + { + if ($items instanceof static) { + $items = $items->toArray(); + } + $this->items = array_merge($this->items, (array) $items); + + return $this; + } + + // Implements export functions to array, YAML and JSON. + + /** + * Return items as an array. + * + * @return array Array presentation of the iterator. + */ + public function toArray() + { + return $this->items; + } + + /** + * Return YAML encoded string of items. + * + * @return string YAML presentation of the iterator. + */ + public function toYaml() + { + return Yaml::dump($this->items); + } + + /** + * Return JSON encoded string of items. + * + * @return string JSON presentation of the iterator. + */ + public function toJson() + { + return json_encode($this->items); + } + + // Implements Iterator. + + /** + * Returns the current element. + * + * @return mixed Can return any type. + */ + public function current() + { + return current($this->items); + } + + /** + * Returns the key of the current element. + * + * @return mixed Returns scalar on success, or NULL on failure. + */ + public function key() + { + return key($this->items); + } + + /** + * Moves the current position to the next element. + * + * @return void + */ + public function next() + { + if ($this->unset) { + // If current item was unset, position is already in the next element (do nothing). + $this->unset = false; + } else { + next($this->items); + } + } + + /** + * Rewinds back to the first element of the Iterator. + * + * @return void + */ + public function rewind() + { + $this->unset = false; + reset($this->items); + } + + /** + * This method is called after Iterator::rewind() and Iterator::next() to check if the current position is valid. + * + * @return bool Returns TRUE on success or FALSE on failure. + */ + public function valid() + { + return key($this->items) !== null; + } + + // Implements ArrayAccess + + /** + * Whether or not an offset exists. + * + * @param mixed $offset An offset to check for. + * @return bool Returns TRUE on success or FALSE on failure. + */ + public function offsetExists($offset) + { + return isset($this->items[$offset]); + } + + /** + * Returns the value at specified offset. + * + * @param mixed $offset The offset to retrieve. + * @return mixed Can return all value types. + */ + public function offsetGet($offset) + { + return isset($this->items[$offset]) ? $this->items[$offset] : null; + } + + /** + * Assigns a value to the specified offset. + * + * @param mixed $offset The offset to assign the value to. + * @param mixed $value The value to set. + */ + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + /** + * Unsets an offset. + * + * @param mixed $offset The offset to unset. + */ + public function offsetUnset($offset) + { + if ($offset == key($this->items)) { + $this->unset = true; + } + unset($this->items[$offset]); + } + + // Implements Countable + + /** + * This method is executed when using the count() function. + * + * @return int The count of items. + */ + public function count() + { + return count($this->items); + } + + // Implements Serializable + + /** + * Returns string representation of the object. + * + * @return string Returns the string representation of the object. + */ + public function serialize() + { + return serialize($this->items); + } + + /** + * Called during unserialization of the object. + * + * @param string $serialized The string representation of the object. + */ + public function unserialize($serialized) + { + $this->items = unserialize($serialized); + } +} diff --git a/system/src/Grav/Common/Page/Asset.php b/system/src/Grav/Common/Page/Asset.php new file mode 100644 index 000000000..148900cf5 --- /dev/null +++ b/system/src/Grav/Common/Page/Asset.php @@ -0,0 +1,321 @@ +get('type') == 'image') { + $filePath = $this->get('path') . '/' . $this->get('filename'); + $image_info = getimagesize($filePath); + $this->set('thumb', $filePath); + $this->def('width', $image_info[0]); + $this->def('height', $image_info[1]); + $this->def('mime', $image_info['mime']); + $this->reset(); + } else { + $this->def('mime', 'application/octet-stream'); + } + } + + /** + * Return string representation of the object (html or url). + * + * @return string + */ + public function __toString() + { + return $this->linkImage ? $this->html() : $this->url(); + } + + /** + * Return URL to file. + * + * @return string + */ + public function url() + { + $config = Registry::get('Config'); + + if ($this->image) { + $output = $this->image->cacheFile($this->type, $this->quality); + $this->reset(); + } else { + $relPath = preg_replace('|^' . ROOT_DIR . '|', '', $this->get('path')); + $output = $relPath . '/' . $this->get('filename'); + } + + return $config->get('system.base_url_relative') . '/'. $output; + } + + /** + * Sets image output format. + * + * @param string $type + * @param int $quality + */ + public function format($type = null, $quality = 80) + { + if (!$this->image) { + $this->image(); + } + + $this->type = $type; + $this->quality = $quality; + } + + /** + * Returns tag from the asset. + * + * @param string $title + * @param string $class + * @param string $type + * @param int $quality + * @return string + */ + public function img($title = null, $class = null, $type = null, $quality = 80) + { + if (!$this->image) { + $this->image(); + } + + $output = $this->html($title, $class, $type, $quality); + + return $output; + } + + /** + * Return HTML markup from the asset. + * + * @param string $title + * @param string $class + * @param string $type + * @param int $quality + * @return string + */ + public function html($title = null, $class = null, $type = null, $quality = 80) + { + $title = $title ? $title : $this->get('title'); + $class = $class ? $class : ''; + + if ($this->image) { + $type = $type ? $type : $this->type; + $quality = $quality ? $quality : $this->quality; + + $url = $this->url($type, $quality); + $this->reset(); + + $output = '' . $title . ''; + } else { + $output = $title; + } + + if ($this->linkTarget) { + $config = Registry::get('Config'); + + $output = 'linkAttributes. ' class="'. $class . '">' . $output . ''; + + $this->linkTarget = $this->linkAttributes = null; + } + + return $output; + } + + /** + * Return lightbox HTML for the asset. + * + * @param int $width + * @param int $height + * @return $this + */ + public function lightbox($width = null, $height = null) + { + $this->linkAttributes = ' rel="lightbox"'; + + return $this->link($width, $height); + } + + /** + * Return link HTML for the asset. + * + * @param int $width + * @param int $height + * @return $this + */ + public function link($width = null, $height = null) + { + if ($this->image) { + $image = clone $this->image; + if ($width && $height) { + $image->cropResize($width, $height); + } + $this->linkTarget = $image->cacheFile($this->type, $this->quality); + } else { + // TODO: we need to find out URI in a bit better way. + $relPath = preg_replace('|^' . ROOT_DIR . '|', '', $this->get('path')); + $this->linkTarget = $relPath. '/' . $this->get('filename'); + } + + return $this; + } + + /** + * Reset image. + * + * @return $this + */ + public function reset() + { + $this->image = null; + + if ($this->get('type') == 'image') { + $this->image(); + $this->filter(); + } + $this->type = 'guess'; + $this->quality = 80; + + return $this; + } + + /** + * Forward the call to the image processing method. + * + * @param string $method + * @param mixed $args + * @return $this|mixed + */ + public function __call($method, $args) + { + if ($method == 'cropZoom') { + $method = 'zoomCrop'; + } + + // Always initialize image. + if (!$this->image) { + $this->image(); + } + $result = call_user_func_array(array($this->image, $method), $args); + + // Returns either current object or result of the action. + return $result instanceof ImageFile ? $this : $result; + } + + /** + * Gets asset image, resets image manipulation operations. + * + * @param string $variable + * @return $this + */ + public function image($variable = 'thumb') + { + // TODO: add default file + $file = $this->get($variable); + $this->image = ImageFile::open($file) + ->setCacheDir(basename(IMAGES_DIR)) + ->setActualCacheDir(IMAGES_DIR) + ->setPrettyName(basename($this->get('basename'))); + + $this->filter(); + + return $this; + } + + /** + * Add meta file for the asset. + * + * @param $type + * @return $this + */ + public function addMetaFile($type) + { + $this->meta[$type] = $type; + + $path = $this->get('path') . '/' . $this->get('filename') . '.meta.' . $type; + if ($type == 'yaml') { + $this->merge(Yaml::instance($path)->content()); + } elseif (in_array($type, array('jpg', 'jpeg', 'png', 'gif'))) { + $this->set('thumb', $path); + } + $this->reset(); + + return $this; + } + + /** + * Filter image by using user defined filter parameters. + * + * @param string $filter Filter to be used. + */ + public function filter($filter = 'image.filters.default') + { + $filters = (array) $this->get($filter, array()); + foreach ($filters as $params) { + $params = (array) $params; + $method = array_shift($params); + $this->__call($method, $params); + } + } +} diff --git a/system/src/Grav/Common/Page/Assets.php b/system/src/Grav/Common/Page/Assets.php new file mode 100644 index 000000000..f212a7f73 --- /dev/null +++ b/system/src/Grav/Common/Page/Assets.php @@ -0,0 +1,201 @@ +path = $path; + + $iterator = new \DirectoryIterator($path); + + /** @var \DirectoryIterator $info */ + foreach ($iterator as $info) { + // Ignore folders and Markdown files. + if ($info->isDot() || !$info->isFile() || $info->getExtension() == 'md') { + continue; + } + + // Find out the real filename, in case of we are at the metadata. + $filename = $info->getFilename(); + list($basename, $ext, $meta) = $this->getFileParts($filename); + + // Get asset instance creating it if it didn't exist. + $asset = $this->get("{$basename}.{$ext}", true); + if (!$asset) { + continue; + } + + // Assign meta files to the asset. + if ($meta) { + $asset->addMetaFile($meta); + } + } + } + + /** + * Get asset by basename and extension. + * + * @param string $filename + * @param bool $create + * @return Asset|null + */ + public function get($filename, $create = false) + { + if ($create && !isset($this->instances[$filename])) { + $parts = explode('.', $filename); + $ext = array_pop($parts); + $basename = implode('.', $parts); + + /** @var Config $config */ + $config = Registry::get('Config'); + + // Check if asset type has been configured. + $params = $config->get("assets.{$ext}"); + if (!$params) { + return null; + } + + $filePath = $this->path . '/' . $filename; + $params += array( + 'type' => 'file', + 'thumb' => 'assets/thumb.png', + 'mime' => 'application/octet-stream', + 'name' => $filename, + 'filename' => $filename, + 'basename' => $basename, + 'extension' => $ext, + 'path' => $this->path, + 'modified' => filemtime($filePath), + ); + + $lookup = array( + USER_DIR . 'images/', + SYSTEM_DIR . 'images/', + ); + foreach ($lookup as $path) { + if (is_file($path . $params['thumb'])) { + $params['thumb'] = $path . $params['thumb']; + break; + } + } + + $this->add(new Asset($params)); + } + + return isset($this->instances[$filename]) ? $this->instances[$filename] : null; + } + + /** + * Get a list of all assets. + * + * @return array|Asset[] + */ + public function all() + { + return $this->instances; + } + + /** + * Get a list of all image assets. + * + * @return array|Asset[] + */ + public function images() + { + return $this->images; + } + + /** + * Get a list of all video assets. + * + * @return array|Asset[] + */ + public function videos() + { + return $this->videos; + } + + /** + * Get a list of all file assets. + * + * @return array|Asset[] + */ + public function files() + { + return $this->files; + } + + /** + * @internal + */ + protected function add($file) + { + $this->instances[$file->filename] = $file; + switch ($file->type) { + case 'image': + $this->images[$file->filename] = $file; + break; + case 'video': + $this->videos[$file->filename] = $file; + break; + default: + $this->files[$file->filename] = $file; + } + } + + /** + * Get filename, extension and meta part. + * + * @param string $filename + * @return array + */ + protected function getFileParts($filename) + { + $fileParts = explode('.', $filename); + + $name = array_shift($fileParts); + $extension = null; + while (($part = array_shift($fileParts)) !== null) { + if ($part != 'meta') { + if (isset($extension)) { + $name .= '.' . $extension; + } + $extension = $part; + } else { + break; + } + } + $meta = implode('.', $fileParts); + + return array($name, $extension, $meta); + } +} diff --git a/system/src/Grav/Common/Page/Collection.php b/system/src/Grav/Common/Page/Collection.php new file mode 100644 index 000000000..857675af6 --- /dev/null +++ b/system/src/Grav/Common/Page/Collection.php @@ -0,0 +1,187 @@ +params = $params; + $this->pages = $pages ? $pages : Registry::get('Pages'); + } + + public function params() + { + return $this->params; + } + + /** + * Set parameters to the Collection + * + * @param array $params + * @return $this + */ + public function setParams(array $params) + { + $this->params = array_merge($this->params, $params); + return $this; + } + + /** + * Returns current page. + * + * @return Page + */ + public function current() + { + $current = parent::key(); + return $this->pages->get($current); + } + + /** + * Returns current slug. + * + * @return mixed + */ + public function key() + { + $current = parent::current(); + return $current['slug']; + } + + /** + * Returns the value at specified offset. + * + * @param mixed $offset The offset to retrieve. + * @return mixed Can return all value types. + */ + public function offsetGet($offset) + { + return !empty($this->items[$offset]) ? $this->pages->get($offset) : null; + } + + /** + * Remove item from the list. + * + * @param Page|string|null $key + * @throws \InvalidArgumentException + */ + public function remove($key = null) + { + if ($key instanceof Page) { + $key = $key->path(); + } elseif (is_null($key)) { + $key = key($this->items); + } + if (!is_string($key)) { + throw new \InvalidArgumentException('Invalid argument $key.'); + } + + parent::remove($key); + } + + /** + * Reorder collection. + * + * @param string $by + * @param string $dir + * @param array $manual + * @return $this + */ + public function order($by, $dir = 'asc', $manual = null) + { + $this->items = $this->pages->sortCollection($this, $by, $dir, $manual); + + return $this; + } + + /** + * Check to see if this item is the first in the collection + * @param string $path + * @return boolean True if item is first + */ + public function isFirst($path) + { + if ($this->items && $path == array_keys($this->items)[0]) { + return true; + } else { + return false; + } + } + + /** + * Check to see if this item is the last in the collection + * @param string $path + * @return boolean True if item is last + */ + public function isLast($path) + { + if ($this->items && $path == array_keys($this->items)[count($this->items)-1]) { + return true; + } else { + return false; + } + } + + /** + * Gets the previous sibling based on current position + * + * @return Object the previous item + */ + public function prevSibling($path) + { + return $this->adjacentSibling($path, -1); + } + + /** + * Gets the next sibling based on current position + * + * @return Object the next item + */ + public function nextSibling($path) + { + return $this->adjacentSibling($path, 1); + } + + /** + * Returns the adjacent sibling based on a direction + * @param integer $direction either -1 or +1 + * @return Object the sibling item + */ + public function adjacentSibling($path, $direction = 1) + { + + $values = array_keys($this->items); + $keys = array_flip($values); + $index = $keys[$path] - $direction; + + return isset($values[$index]) ? $this->offsetGet($values[$index]) : $this; + } + + /** + * Returns the item in the current position + * @param String $path the path the item + * @return Object item in the array the the current position + */ + public function currentPosition($path) { + return array_search($path,array_keys($this->items)); + } +} diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php new file mode 100644 index 000000000..dfeaac607 --- /dev/null +++ b/system/src/Grav/Common/Page/Page.php @@ -0,0 +1,1588 @@ +folder to get full path. + */ + protected $path; + + protected $parent; + protected $template; + protected $visible; + protected $slug; + protected $route; + protected $routable; + protected $modified; + protected $id; + protected $header; + protected $content; + protected $raw_content; + protected $pagination; + protected $assets; + protected $title; + protected $max_count; + protected $menu; + protected $date; + protected $taxonomy; + protected $order_by; + protected $order_dir; + protected $order_manual; + protected $modular; + protected $modular_twig; + protected $processing; + protected $summary_size; + + /** + * @var Page Unmodified (original) version of the page. Used for copying and moving the page. + */ + private $_original; + + /** + * @var string Action + */ + private $_action; + + /** + * Page Object Constructor + * + * @param array $array An array of existing page objects + */ + public function __construct($array = array()) + { + /** @var Config $config */ + $config = Registry::get('Config'); + + $this->routable = true; + $this->taxonomy = array(); + $this->processing = $config->get('system.pages.process'); + } + + /** + * Initializes the page instance variables based on a file + * + * @param \SplFileInfo $file The file information for the .md file that the page represents + * @return void + */ + public function init($file) + { + $this->filePath($file->getPathName()); + $this->modified(filemtime($file->getPath())); + $this->id($this->modified().md5($this->filePath())); + $this->header(); + $this->slug(); + $this->visible(); + $this->modularTwig($this->slug[0] == '_'); + } + + /** + * Gets and Sets the raw data + * + * @param string $var Raw content string + * @return Object Raw content string + */ + public function raw($var = null) { + $file = $this->file(); + + if ($var) { + // First update file object. + if ($file) { + $file->raw($var); + } + + // Reset header and content. + $this->modified = time(); + $this->id($this->modified().md5($this->filePath())); + $this->header = null; + $this->content = null; + } + return $file->raw(); + } + + /** + * Gets and Sets the header based on the YAML configuration at the top of the .md file + * + * @param object|array $var a YAML object representing the configuration for the file + * @return object the current YAML configuration + */ + public function header($var = null) + { + if ($var) { + $this->header = (object) $var; + + // Update also file object. + $file = $this->file(); + if ($file) { + $file->header((array) $var); + } + + // Force content re-processing. + $this->id(time().md5($this->filePath())); + } + if (!$this->header) { + $file = $this->file(); + if ($file) { + $this->raw_content = $file->markdown(); + $this->header = (object) $file->header(); + + $var = true; + } + } + + if ($var) { + if (isset($this->header->slug)) { + $this->slug = trim($this->header->slug); + } + if (isset($this->header->title)) { + $this->title = trim($this->header->title); + } + if (isset($this->header->template)) { + $this->template = trim($this->header->template); + } + if (isset($this->header->menu)) { + $this->menu = trim($this->header->menu); + } + if (isset($this->header->routable)) { + $this->routable = $this->header->routable; + } + if (isset($this->header->visible)) { + $this->visible = $this->header->visible; + } + if (isset($this->header->modular)) { + $this->modular = $this->header->modular; + } + if (isset($this->header->order_dir)) { + $this->order_dir = trim($this->header->order_dir); + } + if (isset($this->header->order_by)) { + $this->order_by = trim($this->header->order_by); + } + if (isset($this->header->order_manual)) { + $this->order_manual = (array)$this->header->order_manual; + } + if (isset($this->header->date)) { + $this->date = strtotime($this->header->date); + } + if (isset($this->header->taxonomy)) { + foreach ($this->header->taxonomy as $taxonomy => $taxitems) { + $this->taxonomy[$taxonomy] = (array)$taxitems; + } + } + if (isset($this->header->max_count)) { + $this->max_count = intval($this->header->max_count); + } + if (isset($this->header->processing)) { + foreach ($this->header->processing as $process => $status) { + $this->processing[$process] = $status; + } + } + } + return $this->header; + } + + /** + * Get the summary. + * + * @param int $size Max summary size. + * @return string + */ + public function summary($size = null) + { + + $content = $this->content(); + + // Return calculated summary based on summary divider's position + if (!$size && isset($this->summary_size)) { + return substr($content, 0, $this->summary_size); + } + + // Return calculated summary based on setting in site config file + /** @var Config $config */ + $config = Registry::get('Config'); + if (!$size && $config->get('site.summary.size')) { + $size = $config->get('site.summary.size'); + } + + // Return calculated summary based on defaults + if (!$size) { + $size = 300; + } + + return Utils::truncateHTML($content, $size); + } + + /** + * Gets and Sets the content based on content portion of the .md file + * + * @param string $var Content + * @return string Content + */ + public function content($var = null) + { + if ($var !== null) { + $this->raw_content = $var; + + // Update file object. + $file = $this->file(); + if ($file) { + $file->markdown($var); + } + + // Force re-processing. + $this->id(time().md5($this->filePath())); + $this->content = null; + } + + // If no content, process it + if ($this->content === null) { + + // Load cached content + /** @var Cache $cache */ + $cache = Registry::get('Cache'); + $cache_id = md5('page'.$this->id()); + $content = $cache->fetch($cache_id); + + $update_cache = false; + if ($content === false) { + // Process Markdown + $content = $this->processMarkdown(); + $update_cache = true; + } + + // Process Twig if enabled + if ($this->shouldProcess('twig')) { + + // Always process twig if caching in the page is disabled + $process_twig = (isset($this->header->cache_enable) && !$this->header->cache_enable); + + // Do we want to cache markdown, but process twig in each page? + if ($update_cache && $process_twig) { + $cache->save($cache_id, $content); + $update_cache = false; + } + + // Do we need to process twig this time? + if ($update_cache || $process_twig) { + /** @var Twig $twig */ + $twig = Registry::get('Twig'); + $content = $twig->processPage($this, $content); + } + } + + // Cache the whole page, including processed content + if ($update_cache) { + $cache->save($cache_id, $content); + } + + // Handle summary divider + $divider_pos = strpos($content, '

'.SUMMARY_DELIMITER.'

'); + if ($divider_pos !== false) { + $this->summary_size = $divider_pos; + $content = str_replace('

'.SUMMARY_DELIMITER.'

', '', $content); + } + + $this->content = $content; + + $this->assets(); + } + + return $this->content; + } + + /** + * Get value from a page variable (used mostly for creating edit forms). + * + * @param string $name Variable name. + * @param mixed $default + * @return mixed + */ + public function value($name, $default = null) + { + if ($name == 'content') { + return $this->raw_content; + } + if ($name == 'route') { + return dirname($this->route()); + } + if ($name == 'order') { + $order = $this->order(); + return $order ? (int) $this->order() : ''; + } + if ($name == 'folder') { + $regex = '/^[0-9]+\./u'; + return preg_replace($regex, '', $this->folder); + } + if ($name == 'type') { + return basename($this->name(), '.md'); + } + if ($name == 'assets') { + return $this->assets()->all(); + } + if ($name == 'assets.file') { + return $this->assets()->files(); + } + if ($name == 'assets.video') { + return $this->assets()->videos(); + } + if ($name == 'assets.image') { + return $this->assets()->images(); + } + + $path = explode('.', $name); + $scope = array_shift($path); + + if ($scope == 'header') { + $current = $this->header(); + foreach ($path as $field) { + if (is_object($current) && isset($current->{$field})) { + $current = $current->{$field}; + } elseif (is_array($current) && isset($current[$field])) { + $current = $current[$field]; + } else { + return $default; + } + } + + return $current; + } + + return $default; + } + + /** + * Get file object to the page. + * + * @return File\Markdown|null + */ + public function file() + { + if ($this->name) { + return File\Markdown::instance($this->filePath()); + } + return null; + } + + /** + * Save page if there's a file assigned to it. + * @param bool $reorder Internal use. + */ + public function save($reorder = true) + { + // Perform move, copy or reordering if needed. + $this->doRelocation($reorder); + + $file = $this->file(); + if ($file) { + $file->filename($this->filePath()); + $file->header((array) $this->header()); + $file->markdown($this->content()); + $file->save(); + } + } + + /** + * Prepare move page to new location. Moves also everything that's under the current page. + * + * You need to call $this->save() in order to perform the move. + * + * @param Page $parent New parent page. + * @return Page + */ + public function move(Page $parent) + { + $clone = clone $this; + $clone->_action = 'move'; + $clone->_original = $this; + $clone->parent($parent); + $clone->id(time().md5($clone->filePath())); + // TODO: make sure that the path is in user context. + if ($parent->path()) { + $clone->path($parent->path() . '/' . $clone->folder()); + } + // TODO: make sure we always have the route. + if ($parent->route()) { + $clone->route($parent->route() . '/'. $clone->slug()); + } + + return $clone; + } + + /** + * Prepare a copy from the page. Copies also everything that's under the current page. + * + * Returns a new Page object for the copy. + * You need to call $this->save() in order to perform the move. + * + * @param Page $parent New parent page. + * @return Page + */ + public function copy($parent) + { + $clone = $this->move($parent); + $clone->_action = 'copy'; + + return $clone; + } + + /** + * Get blueprints for the page. + * + * @return Data\Blueprint + */ + public function blueprints() + { + /** @var Pages $pages */ + $pages = Registry::get('Pages'); + + return $pages->blueprints($this->template()); + } + + /** + * Validate page header. + * + * @throws \Exception + */ + public function validate() + { + $blueprints = $this->blueprints(); + $blueprints->validate($this->toArray()); + } + + /** + * Filter page header from illegal contents. + */ + public function filter() + { + $blueprints = $this->blueprints(); + $values = $blueprints->filter($this->toArray()); + $this->header($values['header']); + } + + /** + * Get unknown header variables. + * + * @return array + */ + public function extra() + { + $blueprints = $this->blueprints(); + return $blueprints->extra($this->toArray(), 'header.'); + } + + /** + * Convert page to an array. + * + * @return array + */ + public function toArray() + { + return array( + 'header' => (array) $this->header(), + 'content' => (string) $this->value('content') + ); + } + + /** + * Convert page to YAML encoded string. + * + * @return string + */ + public function toYaml() + { + return Yaml::dump($this->toArray(), 10); + } + + /** + * Convert page to JSON encoded string. + * + * @return string + */ + public function toJson() + { + return json_encode($this->toArray()); + } + + /** + * Gets and sets the associated assets as found in the page folder. + * + * @param Assets $var Representation of associated assets. + * @return Assets Representation of associated assets. + */ + public function assets($var = null) + { + /** @var Cache $cache */ + $cache = Registry::get('Cache'); + + if ($var) { + $this->assets = $var; + } + if ($this->assets === null) { + // Use cached assets if possible. + $assets_cache_id = md5('assets'.$this->id()); + if (!$assets = $cache->fetch($assets_cache_id)) { + $assets = new Assets($this->path()); + $cache->save($assets_cache_id, $assets); + } + $this->assets = $assets; + } + return $this->assets; + } + + /** + * Gets and sets the name field. If no name field is set, it will return 'default.md'. + * + * @param string $var The name of this page. + * @return string The name of this page. + */ + public function name($var = null) + { + if ($var !== null) { + $this->name = $var; + } + return empty($this->name) ? 'default.md' : $this->name; + } + + /** + * Returns child page type. + * + * @return string + */ + public function child_type() + { + return isset($this->header->child_type) ? (string) $this->header->child_type : 'default'; + } + + /** + * Gets and sets the template field. This is used to find the correct Twig template file to render. + * If no field is set, it will return the name without the .md extension + * + * @param string $var the template name + * @return string the template name + */ + public function template($var = null) + { + if ($var !== null) { + $this->template = $var; + } + if (empty($this->template)) { + $this->template = str_replace(CONTENT_EXT, '', $this->name()); + } + return $this->template; + } + + /** + * Gets and sets the title for this Page. If no title is set, it will use the slug() to get a name + * + * @param string $var the title of the Page + * @return string the title of the Page + */ + public function title($var = null) + { + if ($var !== null) { + $this->title = $var; + } + if (empty($this->title)) { + $this->title = ucfirst($this->slug()); + } + return $this->title; + } + + /** + * Gets and sets the menu name for this Page. This is the text that can be used specifically for navigation. + * If no menu field is set, it will use the title() + * + * @param string $var the menu field for the page + * @return string the menu field for the page + */ + public function menu($var = null) + { + if ($var !== null) { + $this->menu = $var; + } + if (empty($this->menu)) { + $this->menu = $this->title(); + } + return $this->menu; + } + + /** + * Gets and Sets whether or not this Page is visible for navigation + * + * @param bool $var true if the page is visible + * @return bool true if the page is visible + */ + public function visible($var = null) + { + if ($var !== null) { + $this->visible = (bool) $var; + } + + if ($this->visible === null) { + // Set item visibility in menu if folder is different from slug + // eg folder = 01.Home and slug = Home + $regex = '/^[0-9]+\./u'; + if (preg_match($regex, $this->folder)) { + $this->visible = true; + } + } + return $this->visible; + } + + /** + * Gets and Sets whether or not this Page is routable, ie you can reach it + * via a URL + * + * @param bool $var true if the page is routable + * @return bool true if the page is routable + */ + public function routable($var = null) + { + if ($var !== null) { + $this->routable = (bool) $var; + } + return $this->routable; + } + + /** + * Gets and Sets the processing setup for this Page. This is multi-dimensional array that consists of + * a simple array of arrays with the form array("markdown"=>true) for example + * + * @param array $var an Array of name value pairs where the name is the process and value is true or false + * @return array an Array of name value pairs where the name is the process and value is true or false + */ + public function processing($var = null) + { + if ($var !== null) { + $this->processing = (array) $var; + } + return $this->processing; + } + + /** + * Gets and Sets the slug for the Page. The slug is used in the URL routing. If not set it uses + * the parent folder from the path + * + * @param string $var the slug, e.g. 'my-blog' + * @return string the slug + */ + public function slug($var = null) + { + if ($var !== null) { + $this->slug = $var; + $baseRoute = $this->parent ? (string) $this->parent()->route() : null; + $this->route = isset($baseRoute) ? $baseRoute . '/'. $this->slug : null; + } + + if (empty($this->slug)) { + $regex = '/^[0-9]+\./u'; + $this->slug = preg_replace($regex, '', $this->folder); + $baseRoute = $this->parent ? (string) $this->parent()->route() : null; + $this->route = isset($baseRoute) ? $baseRoute . '/'. $this->slug : null; + } + return $this->slug; + } + + /** + * Get/set order number of this page. + * + * @param int $var + * @return int|bool + */ + public function order($var = null) + { + $regex = '/^[0-9]+\./u'; + if ($var !== null) { + $order = !empty($var) ? sprintf('%02d.', (int) $var) : ''; + $slug = preg_replace($regex, '', $this->folder); + $this->folder($order.$slug); + } + preg_match($regex, $this->folder, $order); + return isset($order[0]) ? $order[0] : false; + } + + /** + * Gets the URL with host information, aka Permalink. + * @return string The permalink. + */ + public function permalink() + { + return $this->url(true); + } + + /** + * Gets the URL for a page - alias of url(). + * + * @param bool $include_host + * @return string the permalink + */ + public function link($include_host = false) + { + return $this->url($include_host); + } + + /** + * Gets the url for the Page. + * + * @param bool $include_host Defaults false, but true would include http://yourhost.com + * @return string The url. + */ + public function url($include_host = false) + { + /** @var Uri $uri */ + $uri = Registry::get('Uri'); + $rootUrl = $uri->rootUrl($include_host); + $url = $rootUrl.'/'.trim($this->route(), '/'); + + // trim trailing / if not root + if ($url !== '/') { + $url = rtrim($url, '/'); + } + + return $url; + } + + /** + * Gets the route for the page based on the parents route and the current Page's slug. + * + * @param string $var Set new default route. + * + * @return string The route for the Page. + */ + public function route($var = null) + { + if ($var !== null) { + $this->route = $var; + } + return $this->route; + } + + /** + * Gets and sets the identifier for this Page object. + * + * @param string $var the identifier + * @return string the identifier + */ + public function id($var = null) + { + if ($var !== null) { + $this->id = $var; + } + return $this->id; + } + + /** + * Gets and sets the modified timestamp. + * + * @param int $var modified unix timestamp + * @return int modified unix timestamp + */ + public function modified($var = null) + { + if ($var !== null) { + $this->modified = $var; + } + return $this->modified; + } + + /** + * Gets and sets the path to the .md file for this Page object. + * + * @param string $var the file path + * @return string|null the file path + */ + public function filePath($var = null) + { + if ($var !== null) { + // Filename of the page. + $this->name = basename($var); + // Folder of the page. + $this->folder = basename(dirname($var)); + // Path to the page. + $this->path = dirname(dirname($var)); + } + return $this->name ? $this->path . '/' . $this->folder . '/' . $this->name : null; + } + + /** + * Gets and sets the path to the folder where the .md for this Page object resides. + * This is equivalent to the filePath but without the filename. + * + * @param string $var the path + * @return string|null the path + */ + public function path($var = null) + { + if ($var !== null) { + // Folder of the page. + $this->folder = basename($var); + // Path to the page. + $this->path = dirname($var); + } + return $this->path ? $this->path . '/' . $this->folder : null; + } + + /** + * Get/set the folder. + * + * @param string $var Optional path + * @return string|null + */ + public function folder($var = null) + { + if ($var !== null) { + $this->folder = $var; + } + return $this->folder; + } + + /** + * Gets and sets the date for this Page object. This is typically passed in via the page headers + * + * @param string $var string representation of a date + * @return int unix timestamp representation of the date + */ + public function date($var = null) + { + if ($var !== null) { + $this->date = strtotime($var); + } + if (!$this->date) { + $this->date = $this->modified; + } + return $this->date; + } + + /** + * Gets and sets the order by which any sub-pages should be sorted. + * @param string $var the order, either "asc" or "desc" + * @return string the order, either "asc" or "desc" + */ + public function orderDir($var = null) + { + if ($var !== null) { + $this->order_dir = $var; + } + if (empty($this->order_dir)) { + $this->order_dir = 'asc'; + } + return $this->order_dir; + } + + /** + * Gets and sets the order by which the sub-pages should be sorted. + * + * default - is the order based on the file system, ie 01.Home before 02.Advark + * title - is the order based on the title set in the pages + * date - is the order based on the date set in the pages + * folder - is the order based on the name of the folder with any numerics omitted + * + * @param string $var supported options include "default", "title", "date", and "folder" + * @return string supported options include "default", "title", "date", and "folder" + */ + public function orderBy($var = null) + { + if ($var !== null) { + $this->order_by = $var; + } + return $this->order_by; + } + + /** + * Gets the manual order set in the header. + * + * @param string $var supported options include "default", "title", "date", and "folder" + * @return array + */ + public function orderManual($var = null) + { + if ($var !== null) { + $this->order_manual = $var; + } + return (array) $this->order_manual; + } + + /** + * Gets and sets the maxCount field which describes how many sub-pages should be displayed if the + * sub_pages header property is set for this page object. + * + * @param int $var the maximum number of sub-pages + * @return int the maximum number of sub-pages + */ + public function maxCount($var = null) + { + if ($var !== null) { + $this->max_count = (int) $var; + } + if (empty($this->max_count)) { + /** @var Config $config */ + $config = Registry::get('Config'); + $this->max_count = (int) $config->get('system.pages.list.count'); + } + return $this->max_count; + } + + /** + * Gets and sets the taxonomy array which defines which taxonomies this page identifies itself with. + * + * @param array $var an array of taxonomies + * @return array an array of taxonomies + */ + public function taxonomy($var = null) + { + if ($var !== null) { + $this->taxonomy = $var; + } + return $this->taxonomy; + } + + /** + * Gets and sets the modular var that helps identify this parent page contains modular pages. + * + * @param bool $var true if modular_twig + * @return bool true if modular_twig + */ + public function modular($var = null) + { + if ($var !== null) { + $this->modular = (bool) $var; + } + return $this->modular; + } + + /** + * Gets and sets the modular_twig var that helps identify this page as a modular page that will need + * twig processing handled differently from a regular page. + * + * @param bool $var true if modular_twig + * @return bool true if modular_twig + */ + public function modularTwig($var = null) + { + if ($var !== null) { + $this->modular_twig = (bool) $var; + if ($var) { + $this->processing['twig'] = true; + } + } + return $this->modular_twig; + } + + /** + * Gets the configured state of the processing method. + * + * @param string $process the process, eg "twig" or "markdown" + * @return bool whether or not the processing method is enabled for this Page + */ + public function shouldProcess($process) + { + return isset($this->processing[$process]) ? (bool) $this->processing[$process] : false; + } + + /** + * Gets and Sets the parent object for this page + * + * @param Page $var the parent page object + * @return Page|null the parent page object if it exists. + */ + public function parent(Page $var = null) + { + if ($var !== null) { + $this->parent = $var ? $var->path() : ''; + } + /** @var Pages $pages */ + $pages = Registry::get('Pages'); + + return $pages->get($this->parent); + } + + /** + * Returns children of this page. + * + * @return Collection + */ + public function children() + { + /** @var Pages $pages */ + $pages = Registry::get('Pages'); + + return $pages->children($this->path()); + } + + /** + * @throws \Exception + * @deprecated + */ + public function count() + { + throw new \Exception('Use $page->children()->count() instead.'); + } + + /** + * @param $key + * @throws \Exception + * @deprecated + */ + public function __get($key) + { + throw new \Exception('Use $page->children()->__get() instead.'); + } + + /** + * @param $key + * @param $value + * @throws \Exception + * @deprecated + */ + public function __set($key, $value) + { + throw new \Exception('Use $page->children()->__set() instead.'); + } + + /** + * @throws \Exception + * @deprecated + */ + public function current() + { + throw new \Exception('Use $page->children()->current() instead.'); + } + + /** + * @throws \Exception + * @deprecated + */ + public function next() + { + throw new \Exception('Use $page->children()->next() instead.'); + } + + /** + * @throws \Exception + * @deprecated + */ + public function prev() + { + throw new \Exception('Use $page->children()->prev() instead.'); + } + + /** + * @param string $key + * @throws \Exception + * @deprecated + */ + public function nth($key) + { + throw new \Exception('Use $page->children()->nth($position) instead.'); + } + + /** + * Check to see if this item is the first in an array of sub-pages. + * + * @return boolean True if item is first. + */ + public function isFirst() + { + /** @var Pages $pages */ + $pages = Registry::get('Pages'); + $parent = $pages->get($this->parent); + + if ($this->path() == array_values($parent->items)[0]) { + return true; + } else { + return false; + } + } + + /** + * Check to see if this item is the last in an array of sub-pages. + * + * @return boolean True if item is last + */ + public function isLast() + { + /** @var Pages $pages */ + $pages = Registry::get('Pages'); + $parent = $pages->get($this->parent); + + if ($this->path() == array_values($parent->items)[count($parent->items)-1]) { + return true; + } else { + return false; + } + } + + /** + * Gets the previous sibling based on current position. + * + * @return Page the previous Page item + */ + public function prevSibling() + { + return $this->adjacentSibling(-1); + } + + /** + * Gets the next sibling based on current position. + * + * @return Page the next Page item + */ + public function nextSibling() + { + return $this->adjacentSibling(1); + } + + /** + * Returns the adjacent sibling based on a direction. + * + * @param integer $direction either -1 or +1 + * @return Page the sibling page + */ + public function adjacentSibling($direction = 1) + { + /** @var Pages $pages */ + $pages = Registry::get('Pages'); + $parent = $pages->get($this->parent); + $current = $this->slug(); + + $keys = array_flip(array_keys($parent->items)); + $values = array_values($parent->items); + $index = $keys[$current] - $direction; + + return array_key_exists($index, $values) ? $pages->get($values[$index]) : $this; + } + + /** + * Returns whether or not this page is the currently active page requested via the URL. + * + * @return bool True if it is active + */ + public function active() + { + /** @var Uri $uri */ + $uri = Registry::get('Uri'); + if ($this->url() == $uri->url()) { + return true; + } + return false; + } + + /** + * Returns whether or not this URI's URL contains the URL of the active page. + * Or in other words, is this page's URL in the current URL + * + * @return bool True if active child exists + */ + public function activeChild() + { + $uri = Registry::get('Uri'); + if (!$this->home() && (strpos($uri->url(), $this->url()) !== false)) { + return true; + } + return false; + } + + /** + * Returns whether or not this page is the currently configured home page. + * + * @return bool True if it is the homepage + */ + public function home() + { + return $this->find('/') == $this; + } + + /** + * Returns whether or not this page is the root node of the pages tree. + * + * @return bool True if it is the root + */ + public function root() + { + if (!$this->parent && !$this->name and !$this->visible) { + return true; + } else { + return false; + } + } + + /** + * Helper method to return a page. + * + * @param string $url the url of the page + * @return Page page you were looking for if it exists + * @deprecated + */ + public function find($url) + { + /** @var Pages $pages */ + $pages = Registry::get('Pages'); + return $pages->dispatch($url); + } + + /** + * Get a collection of pages in the current context. + * + * @param string|array $params + * @return Collection + * @throws \InvalidArgumentException + */ + public function collection($params = 'content') + { + if (is_string($params)) { + $params = (array) $this->value('header.'.$params); + } elseif (!is_array($params)) { + throw new \InvalidArgumentException('Argument should be either header variable name or array of parameters'); + } + + if (!isset($params['items'])) { + return array(); + } + + $collection = $this->evaluate($params['items']); + if (!$collection instanceof Collection) { + $collection = new Collection(); + } + $collection->setParams($params); + + // TODO: MOVE THIS INTO SOMEWHERE ELSE? + /** @var Uri $uri */ + $uri = Registry::get('Uri'); + /** @var Config $config */ + $config = Registry::get('Config'); + + foreach ((array) $config->get('site.taxonomies') as $taxonomy) { + if ($uri->param($taxonomy)) { + $items = explode(',', $uri->param($taxonomy)); + $collection->setParams(['taxonomies' => [$taxonomy => $items]]); + + foreach ($collection as $page) { + if ($page->modular()) { + continue; + } + foreach ($items as $item) { + if (empty($page->taxonomy[$taxonomy]) + || !in_array($item, $page->taxonomy[$taxonomy])) { + $collection->remove(); + } + } + } + + $config->set('system.cache.enabled', false); + } + } + // TODO: END OF MOVE + + if (isset($params['order'])) { + $by = isset($params['order']['by']) ? $params['order']['by'] : 'default'; + $dir = isset($params['order']['dir']) ? $params['order']['dir'] : 'asc'; + $custom = isset($params['order']['custom']) ? $params['order']['custom'] : null; + $collection->order($by, $dir, $custom); + } + + /** @var Grav $grav */ + $grav = Registry::get('Grav'); + + // New Custom event to handle things like pagination. + $grav->fireEvent('onAfterCollectionProcessed', $collection); + + $params = $collection->params(); + + $limit = isset($params['limit']) ? $params['limit'] : 0; + $start = !empty($params['pagination']) ? ($uri->currentPage() - 1) * $limit : 0; + + if ($limit && $collection->count() > $limit) { + $collection->slice($start, $limit); + } + + return $collection; + } + + /** + * @param string $value + * @return mixed + * @internal + */ + protected function evaluate($value) + { + // Parse command. + if (is_string($value)) { + // Format: @command.param + $cmd = $value; + $params = array(); + } elseif (is_array($value) && count($value) == 1) { + // Format: @command.param: { attr1: value1, attr2: value2 } + $cmd = (string) key($value); + $params = (array) current($value); + } else { + return $value; + } + + // We only evaluate commands which start with @ + if (empty($cmd) || $cmd[0] != '@') { + return $value; + } + + $parts = explode('.', $cmd); + $current = array_shift($parts); + + $results = null; + switch ($current) { + case '@self': + if (!empty($parts)) { + switch ($parts[0]) { + case 'modular': + // FIXME: filter by modular + $results = $this->children(); + break; + case 'children': + // FIXME: filter by non-modular + $results = $this->children(); + break; + } + } + break; + case '@taxonomy': + // Gets a collection of pages by using one of the following formats: + // @taxonomy.category: blog + // @taxonomy.category: [ blog, featured ] + // @taxonomy: { category: [ blog, featured ], level: 1 } + + /** @var Taxonomy $taxonomy_map */ + $taxonomy_map = Registry::get('Taxonomy'); + + if (!empty($parts)) { + $params = [implode('.', $parts) => $params]; + } + $results = $taxonomy_map->findTaxonomy($params); + break; + } + + return $results; + } + + /** + * @throws \Exception + * @deprecated + */ + public function subPages() + { + throw new \Exception('Use $page->collection() instead.'); + } + + /** + * Sorting of sub-pages based on how to sort and the order. + * + * default - is the order based on the filesystem, ie 01.Home before 02.Advark + * title - is the order based on the title set in the pages + * date - is the order based on the date set in the pages + * modified - is the order based on the last modified date of the pages + * slug - is the order based on the URL slug + * + * @param string $order_by The order by which the sub-pages should be sorted "default", "title", "date", "folder" + * @param string $order_dir The order, either "asc" or "desc" + * @return $this|bool This Page object if sub-pages exist, else false + */ + public function sort($order_by = null, $order_dir = null) + { + throw new \Exception('Use $page->children()->sort() instead.'); + } + + /** + * Returns whether or not this Page object has a .md file associated with it or if its just a directory. + * + * @return bool True if its a page with a .md file associated + */ + public function isPage() + { + if ($this->name) { + return true; + } + return false; + } + + /** + * Returns whether or not this Page object is a directory or a page. + * + * @return bool True if its a directory + */ + public function isDir() + { + return !$this->isPage(); + } + + /** + * Returns whether the page exists in the filesystem. + * + * @return bool + */ + public function exists() + { + $file = $this->file(); + return $file && $file->exists(); + } + + /** + * @throws \Exception + */ + public function hasSubPages() + { + throw new \Exception('Use $page->collection()->count() instead.'); + } + + /** + * Process the Markdown if processing is enabled for it. If not, process as 'raw' which simply strips the + * header YAML from the raw, and sends back the content portion. i.e. the bit below the header. + * + * @return string the content for the page + */ + protected function processMarkdown() + { + // Process Markdown if required + $process_method = $this->shouldProcess('markdown') ? 'parseMarkdownContent' : 'rawContent'; + $content = $this->$process_method($this->raw_content); + + return $content; + } + + /** + * Process the raw content. Basically just strips the headers out and returns the rest. + * + * @param string $content Input raw content + * @return string Output content after headers have been stripped + */ + protected function rawContent($content) + { + return $content; + } + + /** + * Process the Markdown content. This strips the headers, the process the resulting content as Markdown. + * + * @param string $content Input raw content + * @return string Output content that has been processed as Markdown + */ + protected function parseMarkdownContent($content) + { + $parsedown = new \Parsedown(); + $content = $parsedown->parse($content); + return $content; + } + + /** + * Cleans the path. + * + * @param string $path the path + * @return string the path + */ + protected function cleanPath($path) + { + $lastchunk = strrchr($path, DS); + if (strpos($lastchunk, ':') !== false) { + $path = str_replace($lastchunk, '', $path); + } + return $path; + } + + /** + * Moves or copies the page in filesystem. + * + * @internal + */ + protected function doRelocation($reorder) + { + if (empty($this->_original)) { + return; + } + + // Do reordering. + if ($reorder && $this->order() != $this->_original->order()) { + /** @var Pages $pages */ + $pages = Registry::get('Pages'); + + $parent = $this->parent(); + + // Extract visible children from the parent page. + $visible = array(); + /** @var Page $page */ + foreach ($parent as $page) { + if ($page->order()) { + $visible[$page->slug] = $page->path(); + } + } + + // List only visible pages. + $list = array_intersect($visible, $pages->sort($parent)); + + // If page was moved, take it out of the list. + if ($this->_action == 'move') { + unset($list[$this->slug()]); + } + + $list = array_values($list); + + // Then add it back to the new location (if needed). + if ($this->order()) { + array_splice($list, min($this->order()-1, count($list)), 0, array($this->path())); + } + + // Reorder all moved pages. + foreach ($list as $order => $path) { + if ($path == $this->path()) { + // Handle current page; we do want to change ordering number, but nothing else. + $this->order($order+1); + } else { + // Handle all the other pages. + $page = $pages->get($path); + + if ($page && $page->exists() && $page->order() != $order+1) { + $page = $page->move($parent); + $page->order($order+1); + $page->save(false); + } + } + } + } + if ($this->_action == 'move' && $this->_original->exists()) { + Folder::move($this->_original->path(), $this->path()); + } + if ($this->_action == 'copy' && $this->_original->exists()) { + Folder::copy($this->_original->path(), $this->path()); + } + + $this->_action = null; + $this->_original = null; + } +} diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php new file mode 100644 index 000000000..2de156167 --- /dev/null +++ b/system/src/Grav/Common/Page/Pages.php @@ -0,0 +1,547 @@ +grav = Registry::get('Grav'); + $this->config = Registry::get('Config'); + + $this->buildPages(); + } + + /** + * Get or set last modification time. + * + * @param int $modified + * @return int|null + */ + public function lastModified($modified = null) + { + if ($modified && $modified > $this->last_modified) { + $this->last_modified = $modified; + } + return $this->last_modified; + } + + /** + * Returns a list of all pages. + * + * @return Page + */ + public function instances() + { + return $this->instances; + } + + /** + * Returns a list of all routes. + * + * @return array + */ + public function routes() + { + return $this->routes; + } + + /** + * Adds a page and assigns a route to it. + * + * @param Page $page Page to be added. + * @param string $route Optional route (uses route from the object if not set). + */ + public function addPage(Page $page, $route = null) + { + if (!isset($this->instances[$page->path()])) { + $this->instances[$page->path()] = $page; + } + $route = $page->route($route); + if ($page->parent()) { + $this->children[$page->parent()->path()][$page->path()] = array('slug' => $page->slug()); + } + $this->routes[$route] = $page->path(); + } + + /** + * Sort sub-pages in a page. + * + * @param Page $page + * @param string $order_by + * @param string $order_dir + * + * @return array + */ + public function sort(Page $page, $order_by = null, $order_dir = null) + { + if ($order_by === null) { + $order_by = $page->orderBy(); + } + if ($order_dir === null) { + $order_dir = $page->orderDir(); + } + + $path = $page->path(); + $children = isset($this->children[$path]) ? $this->children[$path] : array(); + + if (!$children) { + return $children; + } + + if (!isset($this->sort[$path][$order_by])) { + $this->buildSort($path, $children, $order_by, $page->orderManual()); + } + + $sort = $this->sort[$path][$order_by]; + + if ($order_dir != 'asc') { + $sort = array_reverse($sort); + } + + return $sort; + } + + /** + * @param Collection $collection + * @param $orderBy + * @param string $orderDir + * @param null $orderManual + * @return array + * @internal + */ + public function sortCollection(Collection $collection, $orderBy, $orderDir = 'asc', $orderManual = null) + { + $items = $collection->toArray(); + + $lookup = md5(serialize($items)); + if (!isset($this->sort[$lookup][$orderBy])) { + $this->buildSort($lookup, $items, $orderBy, $orderManual); + } + + $sort = $this->sort[$lookup][$orderBy]; + + if ($orderDir != 'asc') { + $sort = array_reverse($sort); + } + + return $sort; + + } + + /** + * Get a page instance. + * + * @param string $path + * @return Page + */ + public function get($path) + { + if (!is_null($path) && !is_string($path)) throw new \Exception(); + return isset($this->instances[(string) $path]) ? $this->instances[(string) $path] : null; + } + + /** + * Get children of the path. + * + * @param string $path + * @return Collection + */ + public function children($path) + { + $children = isset($this->children[(string) $path]) ? $this->children[(string) $path] : array(); + return new Collection($children, array(), $this); + } + + /** + * Dispatch URI to a page. + * + * @param $url + * @param bool $all + * @return Page|null + */ + public function dispatch($url, $all = false) + { + // Fetch page if there's a defined route to it. + $page = isset($this->routes[$url]) ? $this->get($this->routes[$url]) : null; + + // If the page cannot be reached, look into site wide routes. + if (!$all && (!$page || !$page->routable())) { + $route = $this->config->get("site.routes.{$url}"); + if ($route) { + $page = $this->dispatch($route, $all); + } + } + + return $page; + } + + /** + * Get root page. + * + * @return Page + */ + public function root() + { + return $this->instances[rtrim(PAGES_DIR, DS)]; + } + + /** + * Get a blueprint for a page type. + * + * @param string $type + * @return Data\Blueprint + */ + public function blueprints($type) + { + if (!isset($this->blueprints)) { + $this->blueprints = new Data\Blueprints(THEMES_DIR . $this->config->get('system.pages.theme') . '/blueprints/'); + } + + try { + $blueprint = $this->blueprints->get($type); + } catch (\RuntimeException $e) { + $blueprint = $this->blueprints->get('default'); + } + + if (!$blueprint->initialized) { + /** @var Grav $grav */ + $grav = Registry::get('Grav'); + $grav->fireEvent('onCreateBlueprint', $blueprint); + $blueprint->initialized = true; + } + + return $blueprint; + } + + /** + * Get list of route/title of all pages. + * + * @param Page $current + * @param int $level + * @return array + * @throws \RuntimeException + */ + public function getList(Page $current = null, $level = 0) + { + if (!$current) { + if ($level) { + throw new \RuntimeException('Internal error'); + } + + $current = $this->root(); + } + + $list = array(); + if ($current->routable()) { + $list[$current->route()] = str_repeat('  ', ($level-1)*2) . $current->title(); + } + + foreach ($current as $next) { + $list = array_merge($list, $this->getList($next, $level + 1)); + } + + return $list; + } + + /** + * Get available page types. + * + * @return array + */ + static public function types() + { + /** @var Config $config */ + $config = Registry::get('Config'); + $blueprints = new Data\Blueprints(THEMES_DIR . $config->get('system.pages.theme') . '/blueprints/'); + + return $blueprints->types(); + } + + /** + * Get available parents. + * + * @return array + */ + static public function parents() + { + /** @var Pages $pages */ + $pages = Registry::get('Pages'); + return $pages->getList(); + } + + /** + * Builds pages. + * + * @internal + */ + protected function buildPages() + { + $this->sort = array(); + if ($this->config->get('system.cache.enabled')) { + /** @var Cache $cache */ + $cache = Registry::get('Cache'); + /** @var Taxonomy $taxonomy */ + $taxonomy = Registry::get('Taxonomy'); + $last_modified = $this->config->get('system.cache.check.pages', true) + ? Folder::lastModified(PAGES_DIR) : 0; + $page_cache_id = md5(USER_DIR.$last_modified); + + list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cache->fetch($page_cache_id); + if (!$this->instances) { + $this->recurse(); + $this->buildRoutes(); + + // save pages, routes, taxonomy, and sort to cache + $cache->save( + $page_cache_id, + array($this->instances, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort) + ); + } else { + // If pages was found in cache, set the taxonomy + $taxonomy->taxonomy($taxonomy_map); + } + } else { + $this->recurse(); + $this->buildRoutes(); + } + } + + /** + * Recursive function to load & build page relationships. + * + * @param string $directory + * @param null $parent + * @return Page + * @throws \RuntimeException + * @internal + */ + protected function recurse($directory = PAGES_DIR, &$parent = null) + { + $directory = rtrim($directory, DS); + $iterator = new \DirectoryIterator($directory); + $page = new Page; + + $page->path($directory); + $page->parent($parent); + $page->orderDir($this->config->get('system.pages.order.dir')); + $page->orderBy($this->config->get('system.pages.order.by')); + + // Add into instances + if (!isset($this->instances[$page->path()])) { + $this->instances[$page->path()] = $page; + if ($parent && $page->path()) { + $this->children[$parent->path()][$page->path()] = array('slug' => $page->slug()); + } + } else { + throw new \RuntimeException('Fatal error when creating page instances.'); + } + + /** @var \DirectoryIterator $file */ + foreach ($iterator as $file) { + $name = $file->getFilename(); + + if ($file->isFile() && Utils::endsWith($name, CONTENT_EXT)) { + + $page->init($file); + + if ($this->config->get('system.pages.events.page')) { + $this->grav->fireEvent('onAfterPageProcessed', $page); + } + + } elseif ($file->isDir() && !$file->isDot()) { + + if (!$page->path()) { + $page->path($file->getPath()); + } + + $path = $directory.DS.$name; + $child = $this->recurse($path, $page); + + if (Utils::startsWith($name, '_')) { + $child->routable(false); + } + + $this->children[$page->path()][$child->path()] = array('slug' => $child->slug()); + + // set the modified time if not already set + if (!$page->date()) { + $page->date($file->getMTime()); + } + + // set the last modified time on pages + $this->lastModified($file->getMTime()); + + if ($this->config->get('system.pages.events.page')) { + $this->grav->fireEvent('onAfterFolderProcessed', $page); + } + } + } + + // Sort based on Defaults or Page Overridden sort order + $this->children[$page->path()] = $this->sort($page); + + return $page; + } + + /** + * @internal + */ + protected function buildRoutes() + { + /** @var $taxonomy Taxonomy */ + $taxonomy = Registry::get('Taxonomy'); + + // Build routes and taxonomy map. + /** @var $page Page */ + foreach ($this->instances as $page) { + + $parent = $page->parent(); + + if ($parent) { + $route = rtrim($parent->route(), '/') . '/' . $page->slug(); + $this->routes[$route] = $page->path(); + $page->route($route); + } + + if (!empty($route)) { + $taxonomy->addTaxonomy($page); + } else { + $page->routable(false); + } + } + + // Alias and set default route to home page. + $home = trim($this->config->get('system.home.alias'), '/'); + if ($home && isset($this->routes['/' . $home])) { + $this->routes['/'] = $this->routes['/' . $home]; + $this->get($this->routes['/' . $home])->route('/'); + } + } + + /** + * @param string $path + * @param array $pages + * @param string $order_by + * @param array $manual + * @throws \RuntimeException + * @internal + */ + protected function buildSort($path, array $pages, $order_by = 'default', $manual = null) + { + $list = array(); + + foreach ($pages as $key => $info) { + + $child = isset($this->instances[$key]) ? $this->instances[$key] : null; + if (!$child) { + throw new \RuntimeException("Page does not exist: {$key}"); + } + + switch ($order_by) { + case 'title': + $list[$key] = $child->title(); + break; + case 'date': + $list[$key] = $child->date(); + break; + case 'modified': + $list[$key] = $child->modified(); + break; + case 'slug': + $list[$key] = $info['slug']; + break; + case 'basename': + $list[$key] = basename($key); + break; + case 'manual': + case 'default': + default: + $list[$key] = $key; + } + } + + // Sort by the new list. + asort($list); + + // Move manually ordered items into the beginning of the list. Order of the unlisted items does not change. + if (is_array($manual) && !empty($manual)) { + $new_list = array(); + $i = count($manual); + + foreach ($list as $key => $dummy) { + $info = $pages[$key]; + $order = array_search($info['slug'], $manual); + if ($order === false) { + $order = $i++; + } + $new_list[$key] = (int) $order; + } + + $list = $new_list; + + // Apply manual ordering to the list. + asort($list); + } + + foreach ($list as $key => $sort) { + $info = $pages[$key]; + // TODO: order by manual needs a hash from the passed variables if we make this more general. + $this->sort[$path][$order_by][$key] = $info; + } + } +} diff --git a/system/src/Grav/Common/Plugin.php b/system/src/Grav/Common/Plugin.php new file mode 100644 index 000000000..fb25dccdb --- /dev/null +++ b/system/src/Grav/Common/Plugin.php @@ -0,0 +1,26 @@ +config = $config; + } +} diff --git a/system/src/Grav/Common/Plugins.php b/system/src/Grav/Common/Plugins.php new file mode 100644 index 000000000..1be3f9e7a --- /dev/null +++ b/system/src/Grav/Common/Plugins.php @@ -0,0 +1,110 @@ +get('plugins'); + + foreach ($plugins as $plugin => $data) { + if (empty($data['enabled'])) { + // Only load enabled plugins. + continue; + } + + $folder = PLUGINS_DIR . $plugin; + $filePath = $folder . DS . $plugin . PLUGIN_EXT; + if (!is_file($filePath)) { + throw new \RuntimeException(sprintf("Plugin '%s' enabled but not found!", $filePath, $plugin)); + } + + require_once $filePath; + + $pluginClass = 'Grav\\Plugin\\'.ucfirst($plugin).'Plugin'; + + if (!class_exists($pluginClass)) { + throw new \RuntimeException(sprintf("Plugin '%s' class not found!", $plugin)); + } + + $this->plugins[$pluginClass] = new $pluginClass($config); + } + + return $this->plugins; + } + + public function add($plugin) + { + if (is_object($plugin)) { + $this->plugins[get_class($plugin)] = $plugin; + } + } + + /** + * Return list of all plugin data with their blueprints. + * + * @return array|Data\Data[] + */ + static public function all() + { + $list = array(); + $iterator = new \DirectoryIterator(PLUGINS_DIR); + + /** @var \DirectoryIterator $directory */ + foreach ($iterator as $directory) { + if (!$directory->isDir() || $directory->isDot()) { + continue; + } + + $type = $directory->getBasename(); + $list[$type] = self::get($type); + } + + ksort($list); + + return $list; + } + + static public function get($type) + { + $blueprints = new Data\Blueprints(PLUGINS_DIR . $type); + $blueprint = $blueprints->get('blueprints'); + $blueprint->name = $type; + + // Load default configuration. + $file = File\Yaml::instance(PLUGINS_DIR . "{$type}/{$type}" . YAML_EXT); + $obj = new Data\Data($file->content(), $blueprint); + + // Override with user configuration. + $file = File\Yaml::instance(USER_DIR . "config/plugins/{$type}" . YAML_EXT); + $obj->merge($file->content()); + + // Save configuration always to user/config. + $obj->file($file); + + return $obj; + } +} diff --git a/system/src/Grav/Common/Registry.php b/system/src/Grav/Common/Registry.php new file mode 100644 index 000000000..59fbe5d7a --- /dev/null +++ b/system/src/Grav/Common/Registry.php @@ -0,0 +1,98 @@ +registry[$key])) { + throw new \Exception("There is no entry for key " . $key); + } + + return self::$instance->registry[$key]; + } + + /** + * @internal + */ + private function __construct() + { + } + + /** + * @internal + */ + private function __clone() + { + } + + /** + * Store entry to the registry. + * + * @param string $key + * @param mixed $value + * @throws \Exception + */ + public function store($key, $value) + { + if (isset($this->registry[$key])) { + throw new \Exception("There is already an entry for key " . $key); + } + + $this->registry[$key] = $value; + } + + /** + * Get entry from the registry. + * + * @param string $key + * @return mixed + * @throws \Exception + */ + public function retrieve($key) + { + if (!isset($this->registry[$key])) { + throw new \Exception("There is no entry for key " . $key); + } + + return $this->registry[$key]; + } +} diff --git a/system/src/Grav/Common/Session/Message.php b/system/src/Grav/Common/Session/Message.php new file mode 100644 index 000000000..1039c527b --- /dev/null +++ b/system/src/Grav/Common/Session/Message.php @@ -0,0 +1,91 @@ + $message, 'scope' => $scope); + + $this->messages[] = $message; + + return $this; + } + + /** + * Clear message queue. + * + * @param string $scope + * @return $this + */ + public function clear($scope = null) + { + if ($scope === null) { + $this->messages = array(); + } else { + foreach ($this->messages as $key => $message) { + if ($message['scope'] == $scope) { + unset($this->messages[$key]); + } + } + } + return $this; + } + + /** + * Fetch all messages. + * + * @param string $scope + * @return array + */ + public function all($scope = null) + { + if ($scope === null) { + return array_values($this->messages); + } + + $messages = array(); + foreach ($this->messages as $message) { + if ($message['scope'] == $scope) { + $messages[] = $message; + } + } + + return $messages; + } + + /** + * Fetch and clear message queue. + * + * @param string $scope + * @return array + */ + public function fetch($scope = null) + { + $messages = $this->all($scope); + $this->clear($scope); + + return $messages; + } + +} diff --git a/system/src/Grav/Common/Session/Session.php b/system/src/Grav/Common/Session/Session.php new file mode 100644 index 000000000..0fe34d7f9 --- /dev/null +++ b/system/src/Grav/Common/Session/Session.php @@ -0,0 +1,245 @@ +started = true; + + return $this; + } + + /** + * Get session ID + * + * @return string Session ID + */ + public function getId() + { + return session_id(); + } + + /** + * Set session Id + * + * @param string $id Session ID + * + * @return $this + */ + public function setId($id) + { + session_id($id); + + return $this; + } + + + /** + * Get session name + * + * @return string + */ + public function getName() + { + return session_name(); + } + + /** + * Set session name + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + session_name($name); + + return $this; + } + + /** + * Invalidates the current session. + * + * @return $this + */ + public function invalidate() + { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, + $params['path'], $params['domain'], + $params['secure'], $params['httponly'] + ); + + session_unset(); + session_destroy(); + + $this->started = false; + + return $this; + } + + /** + * Force the session to be saved and closed + * + * @return $this + */ + public function close() + { + session_write_close(); + + $this->started = false; + + return $this; + } + + /** + * Checks if an attribute is defined. + * + * @param string $name The attribute name + * + * @return bool True if the attribute is defined, false otherwise + */ + public function __isset($name) + { + return isset($_SESSION[$name]); + } + + /** + * Returns an attribute. + * + * @param string $name The attribute name + * + * @return mixed + */ + public function __get($name) + { + return isset($_SESSION[$name]) ? $_SESSION[$name] : null; + } + + /** + * Sets an attribute. + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + $_SESSION[$name] = $value; + } + + /** + * Removes an attribute. + * + * @param string $name + * + * @return mixed The removed value or null when it does not exist + */ + public function __unset($name) + { + unset($_SESSION[$name]); + } + + /** + * Returns attributes. + * + * @return array Attributes + */ + public function all() + { + return $_SESSION; + } + + + /** + * Retrieve an external iterator + * + * @return \ArrayIterator Return an ArrayIterator of $_SESSION + */ + public function getIterator() + { + return new \ArrayIterator($_SESSION); + } + + /** + * Checks if the session was started. + * + * @return Boolean + */ + public function started() + { + return $this->started; + } +} diff --git a/system/src/Grav/Common/Taxonomy.php b/system/src/Grav/Common/Taxonomy.php new file mode 100644 index 000000000..3143e2d4e --- /dev/null +++ b/system/src/Grav/Common/Taxonomy.php @@ -0,0 +1,99 @@ +taxonomy_map = array(); + } + + /** + * Takes an individual page and processes the taxonomies configured in its header. It + * then adds those taxonomies to the map + * + * @param Page\Page $page the page to process + * @param array $page_taxonomy + */ + public function addTaxonomy(Page\Page $page, $page_taxonomy = null) + { + if (!$page_taxonomy) { + $page_taxonomy = $page->taxonomy(); + } + + $config = Registry::get('Config'); + if ($config->get('site.taxonomies') && count($page_taxonomy) > 0) { + foreach ((array) $config->get('site.taxonomies') as $taxonomy) { + if (isset($page_taxonomy[$taxonomy])) { + foreach ((array) $page_taxonomy[$taxonomy] as $item) { + // TODO: move to pages class? + $this->taxonomy_map[$taxonomy][(string) $item][$page->path()] = array('slug' => $page->slug()); + } + } + } + } + } + + /** + * Returns a new Page object with the sub-pages containing all the values set for a + * particular taxonomy. + * + * @param array $taxonomies taxonomies to search, eg ['tag'=>['animal','cat']] + * @return Page\Page page object with sub-pages set to contain matches found in the taxonomy map + */ + public function findTaxonomy($taxonomies) + { + $results = array(); + + foreach ((array)$taxonomies as $taxonomy => $items) { + foreach ((array) $items as $item) { + if (isset($this->taxonomy_map[$taxonomy][$item])) { + $results = array_merge($results, $this->taxonomy_map[$taxonomy][$item]); + } + } + } + + return new Page\Collection($results, ['taxonomies' => $taxonomies]); + } + + /** + * Gets and Sets the taxonomy map + * + * @param array $var the taxonomy map + * @return array the taxonomy map + */ + public function taxonomy($var = null) + { + if ($var) { + $this->taxonomy_map = $var; + } + return $this->taxonomy_map; + } +} diff --git a/system/src/Grav/Common/Theme.php b/system/src/Grav/Common/Theme.php new file mode 100644 index 000000000..31e5cf477 --- /dev/null +++ b/system/src/Grav/Common/Theme.php @@ -0,0 +1,6 @@ +isDir() || $directory->isDot()) { + continue; + } + + $type = $directory->getBasename(); + $list[$type] = self::get($type); + } + + ksort($list); + + return $list; + } + + /** + * Get theme or throw exception if it cannot be found. + * + * @param string $type + * @return Data\Data + * @throws \RuntimeException + */ + static public function get($type) + { + if (!$type) { + throw new \RuntimeException('Theme name not provided.'); + } + + $blueprints = new Data\Blueprints(THEMES_DIR . $type); + $blueprint = $blueprints->get('blueprints'); + $blueprint->name = $type; + + // Find thumbnail. + $thumb = THEMES_DIR . "{$type}/thumbnail.jpg"; + if (file_exists($thumb)) { + // TODO: use real URL with base path. + $blueprint->set('thumbnail', "/user/themes/{$type}/thumbnail.jpg"); + } + + // Load default configuration. + $file = File\Yaml::instance(THEMES_DIR . "{$type}/{$type}" . YAML_EXT); + $obj = new Data\Data($file->content(), $blueprint); + + // Override with user configuration. + $file = File\Yaml::instance(USER_DIR . "config/themes/{$type}" . YAML_EXT); + $obj->merge($file->content()); + + // Save configuration always to user/config. + $obj->file($file); + + return $obj; + } + + public function load($name = null) + { + if (!$name) { + $config = Registry::get('Config'); + $name = $config->get('system.pages.theme'); + } + + $file = THEMES_DIR . "{$name}/{$name}.php"; + if (file_exists($file)) { + require_once $file; + + $className = '\\Grav\\Theme\\' . ucfirst($name); + + if (class_exists($className)) { + $class = new $className; + } + } + + if (empty($class)) { + $class = new Theme; + } + + return $class; + } +} diff --git a/system/src/Grav/Common/Twig.php b/system/src/Grav/Common/Twig.php new file mode 100644 index 000000000..e90ac9e32 --- /dev/null +++ b/system/src/Grav/Common/Twig.php @@ -0,0 +1,241 @@ +twig)) { + + // get Grav and Config + $this->grav = Registry::get('Grav'); + $this->config = $this->grav->config; + $this->uri = Registry::get('Uri'); + $this->taxonomy = Registry::get('Taxonomy'); + + + $this->twig_paths = array(THEMES_DIR . $this->config->get('system.pages.theme') . '/templates'); + $this->grav->fireEvent('onAfterTwigTemplatesPaths'); + + $this->loader = new \Twig_Loader_Filesystem($this->twig_paths); + $loader_chain = new \Twig_Loader_Chain(array($this->loader, new \Twig_Loader_String())); + + $params = $this->config->get('system.twig'); + if (!empty($params['cache'])) { + $params['cache'] = CACHE_DIR; + } + + $this->twig = new \Twig_Environment($loader_chain, $params); + $this->grav->fireEvent('onAfterTwigInit'); + + // set default date format if set in config + if ($this->config->get('system.pages.dateformat.long')) { + $this->twig->getExtension('core')->setDateFormat($this->config->get('system.pages.dateformat.long')); + } + // enable the debug extension if required + if ($this->config->get('system.twig.debug')) { + $this->twig->addExtension(new \Twig_Extension_Debug()); + } + $this->twig->addExtension(new TwigExtension()); + $this->grav->fireEvent('onAfterTwigExtensions'); + + $baseUrlAbsolute = $this->config->get('system.base_url_absolute'); + $baseUrlRelative = $this->config->get('system.base_url_relative'); + $theme = $this->config->get('system.pages.theme'); + $themeUrl = $baseUrlRelative .'/'. USER_PATH . basename(THEMES_DIR) .'/'. $theme; + + // Set some standard variables for twig + $this->twig_vars = array( + 'config' => $this->config, + 'uri' => $this->uri, + 'base_dir' => rtrim(ROOT_DIR, '/'), + 'base_url_absolute' => $baseUrlAbsolute, + 'base_url_relative' => $baseUrlRelative, + 'theme_dir' => THEMES_DIR . $theme, + 'theme_url' => $themeUrl, + 'site' => $this->config->get('site'), + 'stylesheets' => array(), + 'scripts' => array(), + 'taxonomy' => $this->taxonomy, + ); + + } + } + + /** + * @return \Twig_Environment + */ + public function twig() + { + return $this->twig; + } + + /** + * @return \Twig_Loader_Filesystem + */ + public function loader() + { + return $this->loader; + } + + /** + * Twig process that renders a page item. It supports two variations: + * 1) Handles modular pages by rendering a specific page based on its modular twig template + * 2) Renders individual page items for twig processing before the site rendering + * + * @param Page $item The page item to render + * @param string $content Optional content override + * @return string The rendered output + * @throws \RuntimeException + */ + public function processPage(Page $item, $content = null) + { + $this->init(); + $content = $content !== null ? $content : $item->content(); + + // override the twig header vars for local resolution + $this->grav->fireEvent('onAfterPageTwigVars'); + $twig_vars = $this->twig_vars; + + $twig_vars['page'] = $item; + $twig_vars['assets'] = $item->assets(); + $twig_vars['header'] = $item->header(); + + // Get Twig template layout + if ($item->modularTwig()) { + $twig_vars['content'] = $content; + // FIXME: this is inconsistent with main page. + $template = $this->template('modular/' . $item->template()) . TEMPLATE_EXT; + $output = $this->twig->render($template, $twig_vars); + + if ($template == $output) { + throw new \RuntimeException("Template file '{$template}' cannot be found.", 404); + } + } else { + $output = $this->twig->render($content, $twig_vars); + } + + return $output; + } + + /** + * @param string $string string to render. + * @param array $vars Optional variables + * @return string + */ + public function processString($string, array $vars = array()) + { + $this->init(); + + // override the twig header vars for local resolution + $this->grav->fireEvent('onAfterStringTwigVars'); + $vars += $this->twig_vars; + + return $this->twig->render($string, $vars); + } + + /** + * Twig process that renders the site layout. This is the main twig process that renders the overall + * page and handles all the layout for the site display. + * + * @param string $format Output format (defaults to HTML). + * @return string the rendered output + * @throws \RuntimeException + */ + public function processSite($format = null) + { + $this->init(); + + // set the page now its been processed + $this->grav->fireEvent('onAfterSiteTwigVars'); + $twig_vars = $this->twig_vars; + $pages = $this->grav->pages; + $page = $this->grav->page; + + $twig_vars['pages'] = $pages->root(); + $twig_vars['page'] = $page; + $twig_vars['header'] = $page->header(); + $twig_vars['content'] = $page->content(); + $ext = '.' . ($format ? $format : 'html') . TWIG_EXT; + + // Get Twig template layout + $template = $this->template($page->template() . $ext); + $output = $this->twig->render($template, $twig_vars); + + if ($template == $output) { + throw new \RuntimeException("Template file '{$template}' cannot be found.", 404); + } + + return $output; + } + + /** + * Simple helper method to get the twig template if it has already been set, else return + * the one being passed in + * + * @param string $template the template name + * @return string the template name + */ + public function template($template) + { + if (isset($this->template)) { + return $this->template; + } else { + return $template; + } + } +} diff --git a/system/src/Grav/Common/TwigExtension.php b/system/src/Grav/Common/TwigExtension.php new file mode 100644 index 000000000..a7a9799c1 --- /dev/null +++ b/system/src/Grav/Common/TwigExtension.php @@ -0,0 +1,232 @@ +'.$text.''; + } elseif ($count < 40) { + return ''.$text.''; + } else { + return $text; + } + } + + /** + * Remove disabled objects from the array. If input isn't array, do nothing. + * + * @param array $original + * @return array + */ + public function removeDisabledFilter($original) + { + if (!is_array($original)) { + return $original; + } + $new = array(); + + foreach ($original as $entry) { + if (is_object($entry) && !isset($entry->disabled)) { + $new[] = $entry; + } + } + return $new; + } + + /** + * Returns array in a random order. + * + * @param array $original + * @param int $offset Can be used to return only slice of the array. + * @return array + */ + public function randomizeFilter($original, $offset = 0) + { + if (!is_array($original)) { + return $original; + } + + if ($original instanceof \Traversable) { + $original = iterator_to_array($original, false); + } + + $sorted = array(); + $random = array_slice($original, $offset); + shuffle($random); + + for ($x=0; $x < sizeof($original); $x++) { + if ($x < $offset) { + $sorted[] = $original[$x]; + } else { + $sorted[] = array_shift($random); + } + } + return $sorted; + } + + /** + * Inflector supports following notations: + * + * {{ 'person'|pluralize }} => people + * {{ 'shoes'|singularize }} => shoe + * {{ 'welcome page'|titleize }} => "Welcome Page" + * {{ 'send_email'|camelize }} => SendEmail + * {{ 'CamelCased'|underscorize }} => camel_cased + * {{ 'Something Text'|hyphenize }} => something-text + * {{ 'something text to read'|humanize }} => "Something text to read" + * {{ '181'|monthize}} => 6 + * {{ '10'|ordinalize }} => 10th + * + * @param string $action + * @param string $data + * @param int $count + * @return mixed + */ + public function inflectorFilter($action, $data, $count = null) + { + // TODO: check this and fix the docblock if needed. + $action = $action.'ize'; + + if (in_array( + $action, + array('titleize','camelize','underscorize','hyphenize', 'humanize','ordinalize','monthize') + )) { + return Inflector::$action($data); + } elseif (in_array($action, array('pluralize','singularize'))) { + if ($count) { + return Inflector::$action($data, $count); + } else { + return Inflector::$action($data); + } + } else { + return $data; + } + } + + /** + * Return MD5 hash from the input. + * + * @param string $str + * @return string + */ + public function md5Filter($str) + { + return md5($str); + } + + /** + * Repeat given string x times. + * + * @param string $input + * @param int $multiplier + * @return string + */ + public function repeatFunc($input, $multiplier) + { + return str_repeat($input, $multiplier); + } +} diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php new file mode 100644 index 000000000..24e950846 --- /dev/null +++ b/system/src/Grav/Common/Uri.php @@ -0,0 +1,287 @@ +base = $base; + $this->root = $base . rtrim(substr($_SERVER['PHP_SELF'], 0, strpos($_SERVER['PHP_SELF'], 'index.php')), '/'); + $this->url = $base . $uri; + + $this->init(); + + } + + /** + * Initializes the URI object based on the url set on the object + */ + public function init() + { + // get any params and remove them + $uri = str_replace($this->root, '', $this->url); + + $this->params = array(); + if (strpos($uri, ':')) { + $bits = explode('/', $uri); + $path = array(); + foreach ($bits as $bit) { + if (strpos($bit, ':') !== false) { + $param = explode(':', $bit); + if (count($param) == 2) { + $this->params[$param[0]] = str_replace('%7C', '/', $param[1]); + } + } else { + $path[] = $bit; + } + } + $uri = implode('/', $path); + } + + // remove the extension if there is one set + $parts = pathinfo($uri); + if (strpos($parts['basename'], '.')) { + $uri = rtrim($parts['dirname'], '/').'/'.$parts['filename']; + $this->extension = $parts['extension']; + } + + // set the new url + $this->url = $this->root . $uri; + + // split into bits + $this->bits = parse_url($uri); + + $this->query = array(); + if (isset($this->bits['query'])) { + parse_str($this->bits['query'], $this->query); + } + + $this->paths = array(); + $this->path = $this->bits['path']; + $this->content_path = trim(str_replace($this->base, '', $this->path), '/'); + if ($this->content_path != '') { + $this->paths = explode('/', $this->content_path); + } + } + + /** + * Return URI path. + * + * @param string $id + * @return string + */ + public function paths($id = null) + { + if (isset($id)) { + return $this->paths[$id]; + } else { + return implode('/', $this->paths); + } + } + + /** + * Return route to the current URI. By default route doesn't include base path. + * + * @param bool $absolute True to include full path. + * @param bool $domain True to include domain. Works only if first parameter is also true. + * @return string + */ + public function route($absolute = false, $domain = false) + { + return ($absolute ? $this->rootUrl($domain) : '') . '/' . implode('/', $this->paths); + } + + /** + * Return full query string or a single query attribute. + * + * @param string $id Optional attribute. + * @return string + */ + public function query($id = null) + { + if (isset($id)) { + return $this->query[$id]; + } else { + return http_build_query($this->query); + } + } + + /** + * Return all or a single query parameter as a URI compatible string. + * + * @param string $id Optional parameter name. + * @return null|string + */ + public function params($id = null) + { + $params = null; + if ($id === null) { + $output = array(); + foreach ($this->params as $key => $value) { + $output[] = $key . ':' . $value; + $params = '/'.implode('/', $output); + } + } elseif (isset($this->params[$id])) { + $params = "/{$id}:".$this->params[$id]; + } + + return $params; + } + + /** + * Get URI parameter. + * + * @param string $id + * @return bool|string + */ + public function param($id) + { + if (isset($this->params[$id])) { + return urldecode($this->params[$id]); + } else { + return false; + } + } + + /** + * Return URL. + * + * @param bool $include_host Include hostname. + * @return string + */ + public function url($include_host = false) + { + if ($include_host) { + return $this->url; + } else { + $url = (str_replace($this->base, '', rtrim($this->url, '/'))); + return $url ? $url : '/'; + } + } + + /** + * Return the Path + * + * @return String The path of the URI + */ + public function path() { + return $this->path; + } + + /** + * Return the Extension of the URI + * + * @return String The extension of the URI + */ + public function extension() { + return $this->extension; + } + + /** + * Return the host of the URI + * + * @return String The host of the URI + */ + public function host() { + return $this->host; + } + + /** + * Return the base of the URI + * + * @return String The base of the URI + */ + public function base() { + return $this->base; + } + + /** + * Return root URL to the site. + * + * @param bool $include_host Include hostname. + * @return mixed + */ + public function rootUrl($include_host = false) + { + if ($include_host) { + return $this->root; + } else { + $root = str_replace($this->base, '', $this->root); + return $root; + } + } + + /** + * Return current page number. + * + * @return int + */ + public function currentPage() + { + if (isset($this->params['page'])) { + return $this->params['page']; + } else { + return 1; + } + } + + /** + * Return relative path to the referrer defaulting to current or given page. + * + * @param string $default + * @param string $attributes + * @return string + */ + public function referrer($default = null, $attributes = null) + { + $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; + + // Check that referrer came from our site. + $root = $this->rootUrl(true); + if ($referrer) { + // Referrer should always have host set and it should come from the same base address. + if (stripos($referrer, $root) !== 0) { + $referrer = null; + } + } + + if (!$referrer) { + $referrer = $default ? $default : $this->route(true, true); + } + + if ($attributes) { + $referrer .= $attributes; + } + + // Return relative path. + return substr($referrer, strlen($root)); + } +} diff --git a/system/src/Grav/Common/User/Authentication.php b/system/src/Grav/Common/User/Authentication.php new file mode 100644 index 000000000..3a7af6d8f --- /dev/null +++ b/system/src/Grav/Common/User/Authentication.php @@ -0,0 +1,46 @@ +password); + + // Password needs to be updated, save the file. + if ($result == 2) { + $this->password = Authentication::create($password); + $this->save(); + } + + return (bool) $result; + } + + /** + * Checks user authorisation to the action. + * + * @param string $action + * @return bool + */ + public function authorise($action) + { + return $this->get("access.{$action}") === true; + } +} diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php new file mode 100644 index 000000000..09e564ebd --- /dev/null +++ b/system/src/Grav/Common/Utils.php @@ -0,0 +1,142 @@ +/', '', $text)) <= $length) { + return $text; + } + // splits all html-tags to scanable lines + preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER); + $total_length = strlen($ending); + $open_tags = array(); + $truncate = ''; + foreach ($lines as $line_matchings) { + // if there is any html-tag in this line, handle it and add it (uncounted) to the output + if (!empty($line_matchings[1])) { + // if it's an "empty element" with or without xhtml-conform closing slash + if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) { + // do nothing + // if tag is a closing tag + } else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) { + // delete tag from $open_tags list + $pos = array_search($tag_matchings[1], $open_tags); + if ($pos !== false) { + unset($open_tags[$pos]); + } + // if tag is an opening tag + } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) { + // add tag to the beginning of $open_tags list + array_unshift($open_tags, strtolower($tag_matchings[1])); + } + // add html-tag to $truncate'd text + $truncate .= $line_matchings[1]; + } + // calculate the length of the plain text part of the line; handle entities as one character + $content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', ' ', $line_matchings[2])); + if ($total_length+$content_length> $length) { + // the number of characters which are left + $left = $length - $total_length; + $entities_length = 0; + // search for html entities + if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', $line_matchings[2], $entities, PREG_OFFSET_CAPTURE)) { + // calculate the real length of all entities in the legal range + foreach ($entities[0] as $entity) { + if ($entity[1]+1-$entities_length <= $left) { + $left--; + $entities_length += strlen($entity[0]); + } else { + // no more characters left + break; + } + } + } + $truncate .= substr($line_matchings[2], 0, $left+$entities_length); + // maximum lenght is reached, so get off the loop + break; + } else { + $truncate .= $line_matchings[2]; + $total_length += $content_length; + } + // if the maximum length is reached, get off the loop + if($total_length>= $length) { + break; + } + } + } else { + if (strlen($text) <= $length) { + return $text; + } else { + $truncate = substr($text, 0, $length - strlen($ending)); + } + } + // if the words shouldn't be cut in the middle... + if (!$exact) { + // ...search the last occurance of a space... + $spacepos = strrpos($truncate, ' '); + if (isset($spacepos)) { + // ...and cut the text in this position + $truncate = substr($truncate, 0, $spacepos); + } + } + // add the defined ending to the text + $truncate .= $ending; + if($considerHtml) { + // close all unclosed html-tags + foreach ($open_tags as $tag) { + $truncate .= ''; + } + } + return $truncate; + } +} diff --git a/system/src/Grav/Console/InstallCommand.php b/system/src/Grav/Console/InstallCommand.php new file mode 100644 index 000000000..f3dd5ba0d --- /dev/null +++ b/system/src/Grav/Console/InstallCommand.php @@ -0,0 +1,283 @@ + '/index.php', + '/composer.json' => '/composer.json', + '/bin' => '/bin', + '/system' => '/system', + '/vendor' => '/vendor', + '/user/plugins/error' => '/user/plugins/error', + '/user/plugins/problems' => '/user/plugins/problems', + '/user/themes/antimatter' => '/user/themes/antimatter', + ); + + protected $default_file = "---\ntitle: HomePage\n---\n# HomePage\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque porttitor eu felis sed ornare. Sed a mauris venenatis, pulvinar velit vel, dictum enim. Phasellus ac rutrum velit. Nunc lorem purus, hendrerit sit amet augue aliquet, iaculis ultricies nisl. Suspendisse tincidunt euismod risus, quis feugiat arcu tincidunt eget. Nulla eros mi, commodo vel ipsum vel, aliquet congue odio. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Pellentesque velit orci, laoreet at adipiscing eu, interdum quis nibh. Nunc a accumsan purus."; + + protected $source; + protected $destination; + + protected function configure() + { + $this + ->setName('install') + ->setDescription('Installs the base Grav system') + ->addArgument( + 'destination', + InputArgument::REQUIRED, + 'The destination directory to symlink into' + ) + ->addOption( + 'symlink', + 's', + InputOption::VALUE_NONE, + 'Symlink the base grav system' + ) + ->setHelp(<<install command help create a development environment that uses symbolic links to link the core of grav to the git cloned repository +EOT + ); + $this->source = getcwd(); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->destination = $input->getArgument('destination'); + + // Create a red output option + $output->getFormatter()->setStyle('red', new OutputFormatterStyle('red')); + $output->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan')); + $output->getFormatter()->setStyle('magenta', new OutputFormatterStyle('magenta')); + + // Symlink the Core Stuff + if ($input->getOption('symlink')) { + // Create Some core stuff if it doesn't exist + $this->createDirectories($output); + + // Loop through the symlink mappings and create the symlinks + $this->symlink($output); + + // Copy the Core STuff + } else { + $options = true; + // Create Some core stuff if it doesn't exist + $this->createDirectories($output); + + // Loop through the symlink mappings and copy what otherwise would be symlinks + $this->copy($output); + } + + $this->pages($output); + $this->initFiles($output); + $this->perms($output); + } + + private function createDirectories($output) + { + $output->writeln(''); + $output->writeln('Creating Directories'); + $dirs_created = false; + + if (!file_exists($this->destination)) { + mkdir($this->destination, 0777, true); + } + + foreach ($this->directories as $dir) { + if (!file_exists($this->destination . $dir)) { + $dirs_created = true; + $output->writeln(' ' . $dir . ''); + mkdir($this->destination . $dir, 0777, true); + } + } + + if (!$dirs_created) { + $output->writeln(' Directories already exist'); + } + } + + private function copy($output) + { + $output->writeln(''); + $output->writeln('Copying Files'); + + + foreach ($this->mappings as $source => $target) { + if ((int) $source == $source) { + $source = $target; + } + + $from = $this->source . $source; + $to = $this->destination . $target; + + $output->writeln(' ' . $source . ' -> ' . $to); + $this->rcopy($from, $to); + } + } + + private function symlink($output) + { + $output->writeln(''); + $output->writeln('Resetting Symbolic Links'); + + + foreach ($this->mappings as $source => $target) { + if ((int) $source == $source) { + $source = $target; + } + + $from = $this->source . $source; + $to = $this->destination . $target; + + $output->writeln(' ' . $source . ' -> ' . $to); + @unlink ($to); + symlink ($from, $to); + } + } + + private function initFiles($output) + { + $this->check($output); + + $output->writeln(''); + $output->writeln('File Initializing'); + $files_init = false; + + // Copy files if they do not exist + foreach ($this->files as $source => $target) { + if ((int) $source == $source) { + $source = $target; + } + + $from = $this->source . $source; + $to = $this->destination . $target; + + if (!file_exists($to)) { + $files_init = true; + copy($from, $to); + $output->writeln(' '.$target.' -> Created'); + } + } + + if (!$files_init) { + $output->writeln(' Files already exist'); + } + + + } + + private function pages($output) + { + $output->writeln(''); + $output->writeln('Pages Initializing'); + + // get pages files and initialize if no pages exist + $pages_dir = $this->destination . '/user/pages'; + $pages_files = array_diff(scandir($pages_dir), array('..', '.')); + + if (count($pages_files) == 0) { + $destination = $this->source . '/user/pages'; + $this->rcopy($destination, $pages_dir); + $output->writeln(' '.$destination.' -> Created'); + + } + } + + private function perms($output) + { + $output->writeln(''); + $output->writeln('Permisions Initializing'); + + $dir_perms = 0755; + + // get pages files and initialize if no pages exist + chmod($this->destination.'/bin/grav', $dir_perms); + $output->writeln(' bin/grav permissions reset to '. decoct($dir_perms)); + } + + + private function check($output) + { + $success = true; + + if (!file_exists($this->destination)) { + $output->writeln(' file: $this->destination does not exist!'); + $success = false; + } + + foreach ($this->directories as $dir) { + if (!file_exists($this->destination . $dir)) { + $output->writeln(' directory: ' . $dir . ' does not exist!'); + $success = false; + } + } + + foreach ($this->mappings as $target => $link) { + if (!file_exists($this->destination . $target)) { + $output->writeln(' mappings: ' . $target . ' does not exist!'); + $success = false; + } + } + if (!$success) { + $output->writeln(''); + $output->writeln('install should be run with --symlink|--s to symlink first'); + exit; + } + } + + private function rcopy($src, $dest){ + + // If the src is not a directory do a simple file copy + if(!is_dir($src)) { + copy($src, $dest); + return true; + } + + // If the destination directory does not exist create it + if(!is_dir($dest)) { + if(!mkdir($dest)) { + // If the destination directory could not be created stop processing + return false; + } + } + + // Open the source directory to read in files + $i = new \DirectoryIterator($src); + foreach($i as $f) { + if($f->isFile()) { + copy($f->getRealPath(), "$dest/" . $f->getFilename()); + } else if(!$f->isDot() && $f->isDir()) { + $this->rcopy($f->getRealPath(), "$dest/$f"); + } + } + } +} diff --git a/system/src/Grav/Console/PackageCommand.php b/system/src/Grav/Console/PackageCommand.php new file mode 100644 index 000000000..fded91ee9 --- /dev/null +++ b/system/src/Grav/Console/PackageCommand.php @@ -0,0 +1,167 @@ +setName("package") + ->setDescription("Handles packaging chores for Grav") + ->addOption( + 'clean', + 'c', + InputOption::VALUE_NONE, + 'Clean out extra files in vendor folder' + ) + ->setHelp('The package command does things and stuff'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + + + // Create a red output option + $output->getFormatter()->setStyle('red', new OutputFormatterStyle('red')); + $output->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan')); + $output->getFormatter()->setStyle('green', new OutputFormatterStyle('green')); + $output->getFormatter()->setStyle('magenta', new OutputFormatterStyle('magenta')); + + if ($input->getOption('clean')) { + $this->cleanPaths($output); + } + + } + + // loops over the array of paths and deletes the files/folders + private function cleanPaths($output) + { + $output->writeln(''); + $output->writeln('DELETING'); + + foreach($this->paths_to_remove as $path) { + $path = ROOT_DIR . $path; + + if (is_dir($path) && @$this->rrmdir($path)) { + $output->writeln('dir: ' . $path); + } elseif (is_file($path) && @unlink($path)) { + $output->writeln('file: ' . $path); + } + } + + $output->writeln(''); + $output->writeln('CREATING'); + + foreach($this->paths_to_create as $path) { + $path = ROOT_DIR . $path; + if (@mkdir($path)) { + $output->writeln('dir: ' . $path); + } + } + } + + // Recursively Delete folder - DANGEROUS! USE WITH CARE!!!! + private function rrmdir($dir) { + if (is_dir($dir)) { + $objects = scandir($dir); + foreach ($objects as $object) { + if ($object != "." && $object != "..") { + if (filetype($dir."/".$object) == "dir") $this->rrmdir($dir."/".$object); else unlink($dir."/".$object); + } + } + reset($objects); + rmdir($dir); + return true; + } + } +} diff --git a/user/config/site.yaml b/user/config/site.yaml new file mode 100644 index 000000000..dc52b4484 --- /dev/null +++ b/user/config/site.yaml @@ -0,0 +1,6 @@ +title: Grav +author: + name: Joe Bloggs + email: 'joe@test.com' +description: 'Grav is an easy to use, yet powerful, open source flat-file CMS' + diff --git a/user/config/system.yaml b/user/config/system.yaml new file mode 100644 index 000000000..e05c34ba0 --- /dev/null +++ b/user/config/system.yaml @@ -0,0 +1,31 @@ +home: + alias: '/home' + +pages: + theme: antimatter + process: + markdown: true + twig: false + events: + page: false + twig: true + +cache: + enabled: true + check: + pages: true + driver: auto + prefix: 'g' + +twig: + cache: true + debug: true + auto_reload: true + autoescape: false + +debugger: + enabled: true + max_depth: 10 + log: + enabled: false + timing: false diff --git a/user/pages/01.home/default.md b/user/pages/01.home/default.md new file mode 100644 index 000000000..64ea8aeba --- /dev/null +++ b/user/pages/01.home/default.md @@ -0,0 +1,39 @@ +--- +title: Home +--- + +# Grav is Running! +## You have installed **Grav** successfully + +Congratulations! You have installed the **Base Grav Package** that provides a **simple page** and the default **antimatter** theme to get you started. + +>>>>> If you want a more **full-featured** base install, you should check out [**Skeleton** packages available in the downloads](http://getgrav.org/downloads). + +### Find out all about Grav + +* Learn about **Grav** by checking out our dedicated [Learn Grav](http://learn.getgrav.org) site. +* Download **plugins**, **themes**, as well as other Grav **skeleton** packages from the [Grav Downloads](http://getgrav.org/downloads) page. +* Check out our [Grav Development Blog](http://getgrav.org/blog) to find out the latest goings on in the Grav-verse. + +### Edit this Page + +To edit this page, simply navigate to the folder you installed **Grav** into, and then browse to the `user/pages/01.home` folder and open the `default.md` file in your [editor of choice](http://learn.getgrav.org/basics/requirements). You will see the content of this page in [Markdown format](http://learn.getgrav.org/content/markdown). + +### Create a New Page + +Creating a new page is a simple affair in **Grav**. Simply follow these simple steps: + +1. Navigate to your pages folder: `user/pages/` and create a new folder. In this example, we will use [explicit default ordering](http://learn.getgrav.org/content/content-pages) and call the folder `02.mypage`. +2. Launch your text editor and paste in the following sample code: + + --- + title: My New Page + --- + # My New Page! + + This is the body of **my new page** and I can easily use _Markdown_ syntax here. + +3. Save this file in the `user/pages/02.mypage/` folder as `default.md`. This will tell **Grav** to render the page using the **default** template. +4. That is it! Reload your browser to see your new page in the menu. + +>>> NOTE: The page will automatically show up in the Menu after the "Home" menu item. If you wish to change the name that shows up in the Menu, simple add: `menu: My Page` between the dashes in the page content. This is called the YAML front matter, and it is where you configure page-specific options. diff --git a/user/plugins/error/blueprints.yaml b/user/plugins/error/blueprints.yaml new file mode 100644 index 000000000..2d9bb5adc --- /dev/null +++ b/user/plugins/error/blueprints.yaml @@ -0,0 +1,16 @@ +name: Error Page +version: 1.0.0 +description: Displays error page. + +form: + fields: + enabled: + type: toggle + label: Plugin status + highlight: 1 + default: 0 + options: + 1: Enabled + 0: Disabled + validate: + type: bool diff --git a/user/plugins/error/error.php b/user/plugins/error/error.php new file mode 100644 index 000000000..9c80731a7 --- /dev/null +++ b/user/plugins/error/error.php @@ -0,0 +1,46 @@ +page || !$grav->page->routable())) { + + // try to load user error page + $page = $pages->dispatch($this->config->get('error.404', '/error'), true); + + // if none provided use built in + if (!$page) { + $page = new Page; + $page->init(new \SplFileInfo(__DIR__ . '/pages/error.md')); + } + + // Set the page + $grav->page = $page; + } + } + + /** + * Add current directory to twig lookup paths. + */ + public function onAfterTwigTemplatesPaths() + { + Registry::get('Twig')->twig_paths[] = __DIR__ . '/templates'; + } +} diff --git a/user/plugins/error/error.yaml b/user/plugins/error/error.yaml new file mode 100644 index 000000000..9752cae3f --- /dev/null +++ b/user/plugins/error/error.yaml @@ -0,0 +1,6 @@ +name: 404 Not Found +description: 404 Not found + +enabled: true +routes: + 404: '/error' diff --git a/user/plugins/error/pages/error.md b/user/plugins/error/pages/error.md new file mode 100644 index 000000000..129137fb9 --- /dev/null +++ b/user/plugins/error/pages/error.md @@ -0,0 +1,8 @@ +--- +title: Error Page +robots: noindex,nofollow +template: error +routable: false +code: 404 +--- +Woops. Looks like this page doesn't exist. diff --git a/user/plugins/error/templates/error.html.twig b/user/plugins/error/templates/error.html.twig new file mode 100644 index 000000000..02dab9473 --- /dev/null +++ b/user/plugins/error/templates/error.html.twig @@ -0,0 +1,3 @@ +

Error {{ page.header.code }}

+ +

{{ page.content }}

diff --git a/user/plugins/problems/problems.css b/user/plugins/problems/problems.css new file mode 100644 index 000000000..96bd66437 --- /dev/null +++ b/user/plugins/problems/problems.css @@ -0,0 +1,61 @@ +ul.problems { + list-style: none; + padding: 0; + margin-top: 3rem; +} + +ul.problems li { + margin-bottom: 1rem; + padding: 1rem; +} + +ul.problems li.success { + background: #F1F9F1; + border-left: 5px solid #5CB85C; + color: #3d8b3d; +} + +ul.problems li.error { + background: #FDF7F7; + border-left: 5px solid #D9534F; + color: #b52b27; +} + +ul.problems .fa { + font-size: 3rem; + vertical-align: middle; + margin-left: 1rem; + display: block; + float: left; +} + +ul.problems p { + display: block; + margin: 0.5rem 0.5rem 0.5rem 5rem; +} + +.button.big { + font-size: 1.2rem; +} + +.center { + text-align: center; +} + +.underline { + text-decoration: underline; +} + +.clearfix:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +.clearfix { display: inline-block; } +/* start commented backslash hack \*/ +* html .clearfix { height: 1%; } +.clearfix { display: block; } +/* close commented backslash hack */ diff --git a/user/plugins/problems/problems.php b/user/plugins/problems/problems.php new file mode 100644 index 000000000..aff1a15d2 --- /dev/null +++ b/user/plugins/problems/problems.php @@ -0,0 +1,173 @@ +check = CACHE_DIR . $validated_prefix .$cache->getKey(); + + if(!file_exists($this->check)) { + + // Run through potential issues + $this->active = $this->problemChecker(); + + // If no issues remain, save a state file in the cache + if (!$this->active) { + // delete any exising validated files + foreach (glob(CACHE_DIR . $validated_prefix. '*') as $filename) { + unlink($filename); + } + + // create a file in the cache dir so it only runs on cache changes + touch($this->check); + } + + } + + + } + + public function onAfterGetPage() + { + if (!$this->active) { + return; + } + + /** @var Grav $grav */ + $grav = Registry::get('Grav'); + $grav->page->content("# Issues Found\n##Please **Review** and **Resolve** before continuing..."); + + } + + /** + * Add current directory to twig lookup paths. + */ + public function onAfterTwigTemplatesPaths() + { + if (!$this->active) { + return; + } + + Registry::get('Twig')->twig_paths[] = __DIR__ . '/templates'; + } + + /** + * Set needed variables to display the problems. + */ + public function onAfterSiteTwigVars() + { + if (!$this->active) { + return; + } + + $twig = Registry::get('Twig'); + $twig->template = 'problems.html.twig'; + $twig->twig_vars['results'] = $this->results; + + if ($this->config->get('plugins.problems.built_in_css')) { + $twig->twig_vars['stylesheets'][] = 'user/plugins/problems/problems.css'; + } + } + + protected function problemChecker() + { + $min_php_version = '5.4.0'; + $problems_found = false; + + + $essential_files = [ + 'index.php' => false, + '.htaccess' => false, + 'cache' => true, + 'logs' => true, + 'images' => true, + 'system' => false, + 'user/data' => true, + 'user/pages' => false, + 'user/config' => false, + 'user/plugins/error' => false, + 'user/plugins' => false, + 'user/themes' => false, + 'vendor' => false + ]; + + // Check PHP version + if (version_compare(phpversion(), '5.4.0', '<')) { + $problems_found = true; + $php_version_adjective = 'lower'; + $php_version_status = false; + + } else { + $php_version_adjective = 'greater'; + $php_version_status = true; + } + $this->results['php'] = [$php_version_status => 'Your PHP version (' . phpversion() . ') is '. $php_version_adjective . ' than the minimum required: ' . $min_php_version . '']; + + // Check for GD library + if (defined('GD_VERSION') && function_exists('gd_info')) { + $gd_adjective = ''; + $gd_status = true; + } else { + $problems_found = true; + $gd_adjective = 'not '; + $gd_status = false; + } + $this->results['gd'] = [$gd_status => 'PHP GD (Image Manipulation Library) is '. $gd_adjective . 'installed']; + + // Check for essential files & perms + $file_problems = []; + foreach($essential_files as $file => $check_writable) { + $file_path = ROOT_DIR . $file; + if (!file_exists($file_path)) { + $problems_found = true; + $file_status = false; + $file_adjective = 'does not exist'; + + } else { + $file_status = true; + $file_adjective = 'exists'; + $is_writeable = is_writable($file_path); + $is_dir = is_dir($file_path); + + if ($check_writable) { + if (!$is_writeable) { + $file_status = false; + $problems_found = true; + $file_adjective .= ' but is not writeable'; + } else { + $file_adjective .= ' and is writeable'; + } + } + } + if (!$file_status || $is_dir || $check_writable) { + $file_problems[$file_path] = [$file_status => $file_adjective]; + } + } + if (sizeof($file_problems) > 0) { + + $this->results['files'] = $file_problems; + } + + return $problems_found; + } +} diff --git a/user/plugins/problems/problems.yaml b/user/plugins/problems/problems.yaml new file mode 100644 index 000000000..1ab22e745 --- /dev/null +++ b/user/plugins/problems/problems.yaml @@ -0,0 +1,2 @@ +enabled: true +built_in_css: true diff --git a/user/plugins/problems/templates/problems.html.twig b/user/plugins/problems/templates/problems.html.twig new file mode 100644 index 000000000..3d16dc24b --- /dev/null +++ b/user/plugins/problems/templates/problems.html.twig @@ -0,0 +1,55 @@ +{% extends 'partials/base.html.twig' %} + +{% block content %} + {{ page.content }} + +

+ Reload Page +

+ + + + + + + + + <?php echo htmlspecialchars($title) ?> + + + + + + + +
+ +
+
+

getCode() ? ' #' . $exception->getCode() : '') ?>

+ +

getMessage(), ENT_IGNORE) ?> + getMessage())) ?>" rel="noreferrer">search►

+
+ + getPrevious()): ?> + + + + + + + + +
> +

Caused by

+ +
+
+

getCode() ? ' #' . $ex->getCode() : '')) ?>

+ +

getMessage(), ENT_IGNORE) ?>

+
+ + + + + + +
+

+ +
+ +
+ + + + getTrace(); $expanded = NULL ?> + getSeverity(), array(E_USER_NOTICE, E_USER_WARNING, E_USER_DEPRECATED))) && $this->isCollapsed($ex->getFile())) { + foreach ($stack as $key => $row) { + if (isset($row['file']) && !$this->isCollapsed($row['file'])) { $expanded = $key; break; } + } + } ?> + +
+

Source file

+ +
+

File: getFile(), $ex->getLine()) ?>

+ getFile())): ?>getFile(), $ex->getLine(), 15, $ex instanceof \ErrorException && isset($ex->context) ? $ex->context : NULL) ?> +
+ + + + +
+

Call stack

+ +
+
    + $row): ?> +
  1. + + + + + inner-code + + + " class="tracy-toggle">source  + + " ?> + + ' ?> + + + (" class="tracy-toggle tracy-collapsed">arguments) +

    + + +
    id="tracyBsSrc">
    + + + +
    ">
    + + + +
    "> + + getParameters(); + } catch (\Exception $e) { + $params = array(); + } + foreach ($row['args'] as $k => $v) { + echo '\n"; + } + ?> +
    ', htmlspecialchars(isset($params[$k]) ? '$' . $params[$k]->name : "#$k"), ''; + echo Dumper::toHtml($v, array(Dumper::LOCATION => Dumper::LOCATION_CLASS)); + echo "
    +
    + +
  2. + +
+
+ + + + context) && is_array($ex->context)):?> +
+

Variables

+ +
+
+ + context as $k => $v) { + echo '\n"; + } + ?> +
$', htmlspecialchars($k), '', Dumper::toHtml($v, array(Dumper::LOCATION => Dumper::LOCATION_CLASS)), "
+
+
+ + + getPrevious()); ?> +
' ?> + + + + + + +
+

+ +
+ +
+ + + +
+

Environment

+ +
+

$_SERVER

+
+ + $v) echo '\n"; + ?> +
', htmlspecialchars($k), '', Dumper::toHtml($v), "
+
+ + +

$_SESSION

+
+ +

empty

+ + + $v) echo '\n"; + ?> +
', htmlspecialchars($k), '', $k === '__NF' ? 'Nette Session' : Dumper::toHtml($v, array(Dumper::LOCATION => Dumper::LOCATION_CLASS)), "
+ +
+ + + +

Nette Session

+
+ + $v) echo '\n"; + ?> +
', htmlspecialchars($k), '', Dumper::toHtml($v, array(Dumper::LOCATION => Dumper::LOCATION_CLASS)), "
+
+ + + + +

Constants

+
+ + $v) { + echo ''; + echo '\n"; + } + ?> +
', htmlspecialchars($k), '', Dumper::toHtml($v), "
+
+ + + +

Included files ()

+
+ + \n"; + } + ?> +
', htmlspecialchars($v, ENT_IGNORE), "
+
+ + +

Configuration options

+
+ |.+\z#s', '', ob_get_clean()) ?> +
+
+ + +
+

HTTP request

+ +
+ +

Headers

+
+ + $v) echo '\n"; + ?> +
', htmlspecialchars($k), '', htmlspecialchars($v), "
+
+ + + + +

$

+ +

empty

+ +
+ + $v) echo '\n"; + ?> +
', htmlspecialchars($k), '', Dumper::toHtml($v), "
+
+ + +
+ + +
+

HTTP response

+ +
+

Headers

+ +
';
+			?>
+ +

no headers

+ +
+ + + +
+

+ +
+ +
+ + + +
    +
  • Report generated at
  • +
  • +
  • +
+ + +
+
+ + + + diff --git a/vendor/tracy/tracy/src/Tracy/templates/dumper.css b/vendor/tracy/tracy/src/Tracy/templates/dumper.css new file mode 100644 index 000000000..1abe3a121 --- /dev/null +++ b/vendor/tracy/tracy/src/Tracy/templates/dumper.css @@ -0,0 +1,73 @@ +/** + * Dumper + * + * This file is part of the Tracy (http://tracy.nette.org) + * Copyright (c) 2004 David Grudl (http://davidgrudl.com) + */ + +/* toggle */ +.tracy-collapsed { + display: none; +} + +.tracy-toggle.tracy-collapsed { + display: inline; +} + +.tracy-toggle { + cursor: pointer; +} + +.tracy-toggle:after { + content: " ▼"; + opacity: .4; +} + +.tracy-toggle.tracy-collapsed:after { + content: " ►"; +} + + +/* dump */ +pre.tracy-dump { + text-align: left; + color: #444; + background: white; +} + +pre.tracy-dump div { + padding-left: 3ex; +} + +pre.tracy-dump div div { + border-left: 1px solid rgba(0, 0, 0, .1); + margin-left: .5ex; +} + +.tracy-dump-array, .tracy-dump-object { + color: #C22; +} + +.tracy-dump-string { + color: #35D; +} + +.tracy-dump-number { + color: #090; +} + +.tracy-dump-null, .tracy-dump-bool { + color: #850; +} + +.tracy-dump-visibility, .tracy-dump-hash { + font-size: 85%; color: #999; +} + +.tracy-dump-indent { + display: none; +} + +span[data-tracy-href] { + border-bottom: 1px dotted rgba(0, 0, 0, .2); +} diff --git a/vendor/tracy/tracy/src/Tracy/templates/dumper.js b/vendor/tracy/tracy/src/Tracy/templates/dumper.js new file mode 100644 index 000000000..fded2e632 --- /dev/null +++ b/vendor/tracy/tracy/src/Tracy/templates/dumper.js @@ -0,0 +1,54 @@ +/** + * Dumper + * + * This file is part of the Tracy (http://tracy.nette.org) + * Copyright (c) 2004 David Grudl (http://davidgrudl.com) + */ + +(function(){ + + var $ = Tracy.Query.factory; + + var Dumper = Tracy.Dumper = {}; + + Dumper.init = function() { + $(document.body).bind('click', function(e) { + var link; + + // enables & ctrl key + for (link = e.target; link && (!link.getAttribute || !link.getAttribute('data-tracy-href')); link = link.parentNode) {} + if (e.ctrlKey && link) { + location.href = link.getAttribute('data-tracy-href'); + return false; + } + + if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) { + return; + } + + // enables or toggling + link = $(e.target).closest('.tracy-toggle'); + if (!link.length) { + return; + } + var collapsed = link.hasClass('tracy-collapsed'), + ref = link[0].getAttribute('data-ref') || link[0].getAttribute('href', 2), + dest = ref && ref !== '#' ? $(ref) : link.next(''), + panel = link.closest('.tracy-panel'), + oldPosition = panel.position(); + + link[collapsed ? 'removeClass' : 'addClass']('tracy-collapsed'); + dest[collapsed ? 'removeClass' : 'addClass']('tracy-collapsed'); + e.preventDefault(); + + if (panel.length) { + var newPosition = panel.position(); + panel.position({ + right: newPosition.right - newPosition.width + oldPosition.width, + bottom: newPosition.bottom - newPosition.height + oldPosition.height + }); + } + }); + }; + +})(); diff --git a/vendor/tracy/tracy/src/Tracy/templates/error.phtml b/vendor/tracy/tracy/src/Tracy/templates/error.phtml new file mode 100644 index 000000000..a3ae50961 --- /dev/null +++ b/vendor/tracy/tracy/src/Tracy/templates/error.phtml @@ -0,0 +1,36 @@ + + + + + +Server Error + +
+ + +

Server Error

+ +

We're sorry! The server encountered an internal error and +was unable to complete your request. Please try again later.

+ +

error 500Tracy says: ", htmlspecialchars($error) ?>

+
+ + diff --git a/vendor/tracy/tracy/src/Tracy/templates/tracyQ.js b/vendor/tracy/tracy/src/Tracy/templates/tracyQ.js new file mode 100644 index 000000000..7d9a627d9 --- /dev/null +++ b/vendor/tracy/tracy/src/Tracy/templates/tracyQ.js @@ -0,0 +1,294 @@ +/** + * tracyQ + * + * This file is part of the Tracy. + * Copyright (c) 2004, 2014 David Grudl (http://davidgrudl.com) + */ + +var Tracy = Tracy || {}; + +(function(){ + + // supported cross-browser selectors: #id | div | div.class | .class + var Query = Tracy.Query = function(selector) { + if (typeof selector === "string") { + selector = this._find(document, selector); + + } else if (!selector || selector.nodeType || selector.length === undefined || selector === window) { + selector = [selector]; + } + + for (var i = 0, len = selector.length; i < len; i++) { + if (selector[i]) { this[this.length++] = selector[i]; } + } + }; + + Query.factory = function(selector) { + return new Query(selector); + }; + + Query.prototype.length = 0; + + Query.prototype.find = function(selector) { + return new Query(this._find(this[0], selector)); + }; + + Query.prototype._find = function(context, selector) { + if (!context || !selector) { + return []; + + } else if (document.querySelectorAll) { + return context.querySelectorAll(selector); + + } else if (selector.charAt(0) === '#') { // #id + return [document.getElementById(selector.substring(1))]; + + } else { // div | div.class | .class + selector = selector.split('.'); + var elms = context.getElementsByTagName(selector[0] || '*'); + + if (selector[1]) { + var list = [], pattern = new RegExp('(^|\\s)' + selector[1] + '(\\s|$)'); + for (var i = 0, len = elms.length; i < len; i++) { + if (pattern.test(elms[i].className)) { list.push(elms[i]); } + } + return list; + } else { + return elms; + } + } + }; + + Query.prototype.dom = function() { + return this[0]; + }; + + Query.prototype.each = function(callback) { + for (var i = 0; i < this.length; i++) { + if (callback.apply(this[i]) === false) { break; } + } + return this; + }; + + // cross-browser event attach + Query.prototype.bind = function(event, handler) { + if (document.addEventListener && (event === 'mouseenter' || event === 'mouseleave')) { // simulate mouseenter & mouseleave using mouseover & mouseout + var old = handler; + event = event === 'mouseenter' ? 'mouseover' : 'mouseout'; + handler = function(e) { + for (var target = e.relatedTarget; target; target = target.parentNode) { + if (target === this) { return; } // target must not be inside this + } + old.call(this, e); + }; + } + + return this.each(function() { + var elem = this, // fixes 'this' in iE + data = elem.tracy ? elem.tracy : elem.tracy = {}, + events = data.events = data.events || {}; // use own handler queue + + if (!events[event]) { + var handlers = events[event] = [], + generic = function(e) { // dont worry, 'e' is passed in IE + if (!e.target) { + e.target = e.srcElement; + } + if (!e.preventDefault) { + e.preventDefault = function() { e.returnValue = false; }; + } + if (!e.stopPropagation) { + e.stopPropagation = function() { e.cancelBubble = true; }; + } + e.stopImmediatePropagation = function() { this.stopPropagation(); i = handlers.length; }; + for (var i = 0; i < handlers.length; i++) { + handlers[i].call(elem, e); + } + }; + + if (document.addEventListener) { // non-IE + elem.addEventListener(event, generic, false); + } else if (document.attachEvent) { // IE < 9 + elem.attachEvent('on' + event, generic); + } + } + + events[event].push(handler); + }); + }; + + // adds class to element + Query.prototype.addClass = function(className) { + return this.each(function() { + this.className = (this.className.replace(/^|\s+|$/g, ' ').replace(' '+className+' ', ' ') + ' ' + className).replace(/^\s+|\s+$/g,''); + }); + }; + + // removes class from element + Query.prototype.removeClass = function(className) { + return this.each(function() { + this.className = this.className.replace(/^|\s+|$/g, ' ').replace(' '+className+' ', ' ').replace(/^\s+|\s+$/g,''); + }); + }; + + // tests whether element has given class + Query.prototype.hasClass = function(className) { + return this[0] && this[0].className && this[0].className.replace(/^|\s+|$/g, ' ').indexOf(' '+className+' ') > -1; + }; + + Query.prototype.show = function() { + Query.displays = Query.displays || {}; + return this.each(function() { + var tag = this.tagName; + if (!Query.displays[tag]) { + Query.displays[tag] = (new Query(document.body.appendChild(document.createElement(tag)))).css('display'); + } + this.style.display = Query.displays[tag]; + }); + }; + + Query.prototype.hide = function() { + return this.each(function() { + this.style.display = 'none'; + }); + }; + + Query.prototype.css = function(property) { + if (this[0] && this[0].currentStyle) { + return this[0].currentStyle[property]; + } else if (this[0] && window.getComputedStyle) { + return document.defaultView.getComputedStyle(this[0], null).getPropertyValue(property) + } + }; + + Query.prototype.data = function() { + if (this[0]) { + return this[0].tracy ? this[0].tracy : this[0].tracy = {}; + } + }; + + Query.prototype._trav = function(elem, selector, fce) { + selector = selector.split('.'); + while (elem && !(elem.nodeType === 1 && + (!selector[0] || elem.tagName.toLowerCase() === selector[0]) && + (!selector[1] || (new Query(elem)).hasClass(selector[1])))) { + elem = elem[fce]; + } + return new Query(elem || []); + }; + + Query.prototype.closest = function(selector) { + return this._trav(this[0], selector, 'parentNode'); + }; + + Query.prototype.prev = function(selector) { + return this._trav(this[0] && this[0].previousSibling, selector, 'previousSibling'); + }; + + Query.prototype.next = function(selector) { + return this._trav(this[0] && this[0].nextSibling, selector, 'nextSibling'); + }; + + // returns total offset for element + Query.prototype.offset = function(coords) { + if (coords) { + return this.each(function() { + var elem = this, ofs = {left: -coords.left || 0, top: -coords.top || 0}; + while (elem = elem.offsetParent) { + ofs.left += elem.offsetLeft; ofs.top += elem.offsetTop; + } + this.style.left = -ofs.left + 'px'; + this.style.top = -ofs.top + 'px'; + }); + } else if (this[0]) { + var elem = this[0], res = {left: elem.offsetLeft, top: elem.offsetTop}; + while (elem = elem.offsetParent) { + res.left += elem.offsetLeft; res.top += elem.offsetTop; + } + return res; + } + }; + + // returns current position or move to new position + Query.prototype.position = function(coords) { + if (coords) { + return this.each(function() { + if (this.tracy && this.tracy.onmove) { + this.tracy.onmove.call(this, coords); + } + for (var item in coords) { + this.style[item] = coords[item] + 'px'; + } + }); + } else if (this[0]) { + return { + left: this[0].offsetLeft, top: this[0].offsetTop, + right: this[0].style.right ? parseInt(this[0].style.right, 10) : 0, bottom: this[0].style.bottom ? parseInt(this[0].style.bottom, 10) : 0, + width: this[0].offsetWidth, height: this[0].offsetHeight + }; + } + }; + + // makes element draggable + Query.prototype.draggable = function(options) { + var elem = this[0], dE = document.documentElement, started; + options = options || {}; + + (options.handle ? new Query(options.handle) : this).bind('mousedown', function(e) { + var $el = new Query(options.handle ? elem : this); + e.preventDefault(); + e.stopPropagation(); + + if (Query.dragging) { // missed mouseup out of window? + return dE.onmouseup(e); + } + + var pos = $el.position(), + deltaX = options.rightEdge ? pos.right + e.clientX : pos.left - e.clientX, + deltaY = options.bottomEdge ? pos.bottom + e.clientY : pos.top - e.clientY; + + Query.dragging = true; + started = false; + + dE.onmousemove = function(e) { + e = e || window.event; + if (!started) { + if (options.draggedClass) { + $el.addClass(options.draggedClass); + } + if (options.start) { + options.start(e, $el); + } + started = true; + } + + var pos = {}; + pos[options.rightEdge ? 'right' : 'left'] = options.rightEdge ? deltaX - e.clientX : e.clientX + deltaX; + pos[options.bottomEdge ? 'bottom' : 'top'] = options.bottomEdge ? deltaY - e.clientY : e.clientY + deltaY; + $el.position(pos); + return false; + }; + + dE.onmouseup = function(e) { + if (started) { + if (options.draggedClass) { + $el.removeClass(options.draggedClass); + } + if (options.stop) { + options.stop(e || window.event, $el); + } + } + Query.dragging = dE.onmousemove = dE.onmouseup = null; + return false; + }; + + }).bind('click', function(e) { + if (started) { + e.stopImmediatePropagation(); + } + }); + + return this; + }; + +})(); diff --git a/vendor/tracy/tracy/src/shortcuts.php b/vendor/tracy/tracy/src/shortcuts.php new file mode 100644 index 000000000..e8299f628 --- /dev/null +++ b/vendor/tracy/tracy/src/shortcuts.php @@ -0,0 +1,20 @@ +setStub("startBuffering(); +foreach ($iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__ . '/../../src', RecursiveDirectoryIterator::SKIP_DOTS)) as $file) { + echo "adding: {$iterator->getSubPathname()}\n"; + $phar[$iterator->getSubPathname()] = php_strip_whitespace($file); +} + +$phar->stopBuffering(); +$phar->compressFiles(Phar::GZ); + +echo "OK\n"; diff --git a/vendor/tracy/tracy/tools/open-in-editor/install.cmd b/vendor/tracy/tracy/tools/open-in-editor/install.cmd new file mode 100644 index 000000000..ecee89f60 --- /dev/null +++ b/vendor/tracy/tracy/tools/open-in-editor/install.cmd @@ -0,0 +1,8 @@ +@echo off +:: This Windows batch file sets open-editor.js as handler for editor:// protocol + +if defined PROCESSOR_ARCHITEW6432 (set reg="%systemroot%\sysnative\reg.exe") else (set reg=reg) + +%reg% ADD HKCR\editor /ve /d "URL:editor Protocol" /f +%reg% ADD HKCR\editor /v "URL Protocol" /d "" /f +%reg% ADD HKCR\editor\shell\open\command /ve /d "wscript \"%~dp0open-editor.js\" \"%%1\"" /f diff --git a/vendor/tracy/tracy/tools/open-in-editor/open-editor.js b/vendor/tracy/tracy/tools/open-in-editor/open-editor.js new file mode 100644 index 000000000..bc1fd0edd --- /dev/null +++ b/vendor/tracy/tracy/tools/open-in-editor/open-editor.js @@ -0,0 +1,47 @@ +// NetBeans +// var editor = '"C:\\Program Files\\NetBeans 6.9.1\\bin\\netbeans.exe" "%file%:%line%" --console suppress'; + +// PhpStorm +// var editor = '"C:\\Users\\Martin\\AppData\\Roaming\\JetBrains\\PhpStorm 7.1.1\\bin\\PhpStorm.exe" --line %line% "%file%"'; + +// Nusphere PHPEd +// var editor = '"C:\\Program Files\\NuSphere\\PhpED\\phped.exe" "%file%" --line=%line%'; + +// SciTE +// var editor = '"C:\\Program Files\\SciTE\\scite.exe" "-open:%file%" -goto:%line%'; + +// EmEditor +// var editor = '"C:\\Program Files\\EmEditor\\EmEditor.exe" "%file%" /l %line%'; + +// PSPad Editor +// var editor = '"C:\\Program Files\\PSPad editor\\PSPad.exe" -%line% "%file%"'; + +// gVim +// var editor = '"C:\\Program Files\\Vim\\vim73\\gvim.exe" "%file%" +%line%'; + +// Sublime Text 2 +// var editor = '"C:\\Program Files\\Sublime Text 2\\sublime_text.exe" "%file%:%line%"'; + +var mappings = { + // '/remotepath': '/localpath' +}; + +if (typeof editor === 'undefined') { + WScript.Echo('Create variable "editor" in ' + WScript.ScriptFullName); + WScript.Quit(); +} + +var url = WScript.Arguments(0); +var match = /^editor:\/\/open\/\?file=(.+)&line=(\d+)/.exec(url); +if (match) { + var file = decodeURIComponent(match[1]).replace(/\+/g, ' '); + for (var id in mappings) { + if (file.indexOf(id) === 0) { + file = mappings[id] + file.substr(id.length); + break; + } + } + var command = editor.replace(/%line%/g, match[2]).replace(/%file%/g, file); + var shell = new ActiveXObject("WScript.Shell"); + shell.Exec(command.replace(/\\/g, '\\\\')); +} diff --git a/vendor/twig/twig/CHANGELOG b/vendor/twig/twig/CHANGELOG new file mode 100644 index 000000000..76c4e1f1b --- /dev/null +++ b/vendor/twig/twig/CHANGELOG @@ -0,0 +1,688 @@ +* 1.16.1 (2014-XX-XX) + + * n/a + +* 1.16.0 (2014-07-05) + + * changed url_encode to always encode according to RFC 3986 + * fixed inheritance in a 'use'-hierarchy + * removed the __toString policy check when the sandbox is disabled + * fixed recursively calling blocks in templates with inheritance + +* 1.15.1 (2014-02-13) + + * fixed the conversion of the special '0000-00-00 00:00' date + * added an error message when trying to import an undefined block from a trait + * fixed a C extension crash when accessing defined but uninitialized property. + +* 1.15.0 (2013-12-06) + + * made ignoreStrictCheck in Template::getAttribute() works with __call() methods throwing BadMethodCallException + * added min and max functions + * added the round filter + * fixed a bug that prevented the optimizers to be enabled/disabled selectively + * fixed first and last filters for UTF-8 strings + * added a source function to include the content of a template without rendering it + * fixed the C extension sandbox behavior when get or set is prepend to method name + +* 1.14.2 (2013-10-30) + + * fixed error filename/line when an error occurs in an included file + * allowed operators that contain whitespaces to have more than one whitespace + * allowed tests to be made of 1 or 2 words (like "same as" or "divisible by") + +* 1.14.1 (2013-10-15) + + * made it possible to use named operators as variables + * fixed the possibility to have a variable named 'matches' + * added support for PHP 5.5 DateTimeInterface + +* 1.14.0 (2013-10-03) + + * fixed usage of the html_attr escaping strategy to avoid double-escaping with the html strategy + * added new operators: ends with, starts with, and matches + * fixed some compatibility issues with HHVM + * added a way to add custom escaping strategies + * fixed the C extension compilation on Windows + * fixed the batch filter when using a fill argument with an exact match of elements to batch + * fixed the filesystem loader cache when a template name exists in several namespaces + * fixed template_from_string when the template includes or extends other ones + * fixed a crash of the C extension on an edge case + +* 1.13.2 (2013-08-03) + + * fixed the error line number for an error occurs in and embedded template + * fixed crashes of the C extension on some edge cases + +* 1.13.1 (2013-06-06) + + * added the possibility to ignore the filesystem constructor argument in Twig_Loader_Filesystem + * fixed Twig_Loader_Chain::exists() for a loader which implements Twig_ExistsLoaderInterface + * adjusted backtrace call to reduce memory usage when an error occurs + * added support for object instances as the second argument of the constant test + * fixed the include function when used in an assignment + +* 1.13.0 (2013-05-10) + + * fixed getting a numeric-like item on a variable ('09' for instance) + * fixed getting a boolean or float key on an array, so it is consistent with PHP's array access: + `{{ array[false] }}` behaves the same as `echo $array[false];` (equals `$array[0]`) + * made the escape filter 20% faster for happy path (escaping string for html with UTF-8) + * changed ☃ to § in tests + * enforced usage of named arguments after positional ones + +* 1.12.3 (2013-04-08) + + * fixed a security issue in the filesystem loader where it was possible to include a template one + level above the configured path + * fixed fatal error that should be an exception when adding a filter/function/test too late + * added a batch filter + * added support for encoding an array as query string in the url_encode filter + +* 1.12.2 (2013-02-09) + + * fixed the timezone used by the date filter and function when the given date contains a timezone (like 2010-01-28T15:00:00+02:00) + * fixed globals when getGlobals is called early on + * added the first and last filter + +* 1.12.1 (2013-01-15) + + * added support for object instances as the second argument of the constant function + * relaxed globals management to avoid a BC break + * added support for {{ some_string[:2] }} + +* 1.12.0 (2013-01-08) + + * added verbatim as an alias for the raw tag to avoid confusion with the raw filter + * fixed registration of tests and functions as anonymous functions + * fixed globals management + +* 1.12.0-RC1 (2012-12-29) + + * added an include function (does the same as the include tag but in a more flexible way) + * added the ability to use any PHP callable to define filters, functions, and tests + * added a syntax error when using a loop variable that is not defined + * added the ability to set default values for macro arguments + * added support for named arguments for filters, tests, and functions + * moved filters/functions/tests syntax errors to the parser + * added support for extended ternary operator syntaxes + +* 1.11.1 (2012-11-11) + + * fixed debug info line numbering (was off by 2) + * fixed escaping when calling a macro inside another one (regression introduced in 1.9.1) + * optimized variable access on PHP 5.4 + * fixed a crash of the C extension when an exception was thrown from a macro called without being imported (using _self.XXX) + +* 1.11.0 (2012-11-07) + + * fixed macro compilation when a variable name is a PHP reserved keyword + * changed the date filter behavior to always apply the default timezone, except if false is passed as the timezone + * fixed bitwise operator precedences + * added the template_from_string function + * fixed default timezone usage for the date function + * optimized the way Twig exceptions are managed (to make them faster) + * added Twig_ExistsLoaderInterface (implementing this interface in your loader make the chain loader much faster) + +* 1.10.3 (2012-10-19) + + * fixed wrong template location in some error messages + * reverted a BC break introduced in 1.10.2 + * added a split filter + +* 1.10.2 (2012-10-15) + + * fixed macro calls on PHP 5.4 + +* 1.10.1 (2012-10-15) + + * made a speed optimization to macro calls when imported via the "import" tag + * fixed C extension compilation on Windows + * fixed a segfault in the C extension when using DateTime objects + +* 1.10.0 (2012-09-28) + + * extracted functional tests framework to make it reusable for third-party extensions + * added namespaced templates support in Twig_Loader_Filesystem + * added Twig_Loader_Filesystem::prependPath() + * fixed an error when a token parser pass a closure as a test to the subparse() method + +* 1.9.2 (2012-08-25) + + * fixed the in operator for objects that contain circular references + * fixed the C extension when accessing a public property of an object implementing the \ArrayAccess interface + +* 1.9.1 (2012-07-22) + + * optimized macro calls when auto-escaping is on + * fixed wrong parent class for Twig_Function_Node + * made Twig_Loader_Chain more explicit about problems + +* 1.9.0 (2012-07-13) + + * made the parsing independent of the template loaders + * fixed exception trace when an error occurs when rendering a child template + * added escaping strategies for CSS, URL, and HTML attributes + * fixed nested embed tag calls + * added the date_modify filter + +* 1.8.3 (2012-06-17) + + * fixed paths in the filesystem loader when passing a path that ends with a slash or a backslash + * fixed escaping when a project defines a function named html or js + * fixed chmod mode to apply the umask correctly + +* 1.8.2 (2012-05-30) + + * added the abs filter + * fixed a regression when using a number in template attributes + * fixed compiler when mbstring.func_overload is set to 2 + * fixed DateTimeZone support in date filter + +* 1.8.1 (2012-05-17) + + * fixed a regression when dealing with SimpleXMLElement instances in templates + * fixed "is_safe" value for the "dump" function when "html_errors" is not defined in php.ini + * switched to use mbstring whenever possible instead of iconv (you might need to update your encoding as mbstring and iconv encoding names sometimes differ) + +* 1.8.0 (2012-05-08) + + * enforced interface when adding tests, filters, functions, and node visitors from extensions + * fixed a side-effect of the date filter where the timezone might be changed + * simplified usage of the autoescape tag; the only (optional) argument is now the escaping strategy or false (with a BC layer) + * added a way to dynamically change the auto-escaping strategy according to the template "filename" + * changed the autoescape option to also accept a supported escaping strategy (for BC, true is equivalent to html) + * added an embed tag + +* 1.7.0 (2012-04-24) + + * fixed a PHP warning when using CIFS + * fixed template line number in some exceptions + * added an iterable test + * added an error when defining two blocks with the same name in a template + * added the preserves_safety option for filters + * fixed a PHP notice when trying to access a key on a non-object/array variable + * enhanced error reporting when the template file is an instance of SplFileInfo + * added Twig_Environment::mergeGlobals() + * added compilation checks to avoid misuses of the sandbox tag + * fixed filesystem loader freshness logic for high traffic websites + * fixed random function when charset is null + +* 1.6.5 (2012-04-11) + + * fixed a regression when a template only extends another one without defining any blocks + +* 1.6.4 (2012-04-02) + + * fixed PHP notice in Twig_Error::guessTemplateLine() introduced in 1.6.3 + * fixed performance when compiling large files + * optimized parent template creation when the template does not use dynamic inheritance + +* 1.6.3 (2012-03-22) + + * fixed usage of Z_ADDREF_P for PHP 5.2 in the C extension + * fixed compilation of numeric values used in templates when using a locale where the decimal separator is not a dot + * made the strategy used to guess the real template file name and line number in exception messages much faster and more accurate + +* 1.6.2 (2012-03-18) + + * fixed sandbox mode when used with inheritance + * added preserveKeys support for the slice filter + * fixed the date filter when a DateTime instance is passed with a specific timezone + * added a trim filter + +* 1.6.1 (2012-02-29) + + * fixed Twig C extension + * removed the creation of Twig_Markup instances when not needed + * added a way to set the default global timezone for dates + * fixed the slice filter on strings when the length is not specified + * fixed the creation of the cache directory in case of a race condition + +* 1.6.0 (2012-02-04) + + * fixed raw blocks when used with the whitespace trim option + * made a speed optimization to macro calls when imported via the "from" tag + * fixed globals, parsers, visitors, filters, tests, and functions management in Twig_Environment when a new one or new extension is added + * fixed the attribute function when passing arguments + * added slice notation support for the [] operator (syntactic sugar for the slice operator) + * added a slice filter + * added string support for the reverse filter + * fixed the empty test and the length filter for Twig_Markup instances + * added a date function to ease date comparison + * fixed unary operators precedence + * added recursive parsing support in the parser + * added string and integer handling for the random function + +* 1.5.1 (2012-01-05) + + * fixed a regression when parsing strings + +* 1.5.0 (2012-01-04) + + * added Traversable objects support for the join filter + +* 1.5.0-RC2 (2011-12-30) + + * added a way to set the default global date interval format + * fixed the date filter for DateInterval instances (setTimezone() does not exist for them) + * refactored Twig_Template::display() to ease its extension + * added a number_format filter + +* 1.5.0-RC1 (2011-12-26) + + * removed the need to quote hash keys + * allowed hash keys to be any expression + * added a do tag + * added a flush tag + * added support for dynamically named filters and functions + * added a dump function to help debugging templates + * added a nl2br filter + * added a random function + * added a way to change the default format for the date filter + * fixed the lexer when an operator ending with a letter ends a line + * added string interpolation support + * enhanced exceptions for unknown filters, functions, tests, and tags + +* 1.4.0 (2011-12-07) + + * fixed lexer when using big numbers (> PHP_INT_MAX) + * added missing preserveKeys argument to the reverse filter + * fixed macros containing filter tag calls + +* 1.4.0-RC2 (2011-11-27) + + * removed usage of Reflection in Twig_Template::getAttribute() + * added a C extension that can optionally replace Twig_Template::getAttribute() + * added negative timestamp support to the date filter + +* 1.4.0-RC1 (2011-11-20) + + * optimized variable access when using PHP 5.4 + * changed the precedence of the .. operator to be more consistent with languages that implements such a feature like Ruby + * added an Exception to Twig_Loader_Array::isFresh() method when the template does not exist to be consistent with other loaders + * added Twig_Function_Node to allow more complex functions to have their own Node class + * added Twig_Filter_Node to allow more complex filters to have their own Node class + * added Twig_Test_Node to allow more complex tests to have their own Node class + * added a better error message when a template is empty but contain a BOM + * fixed "in" operator for empty strings + * fixed the "defined" test and the "default" filter (now works with more than one call (foo.bar.foo) and for both values of the strict_variables option) + * changed the way extensions are loaded (addFilter/addFunction/addGlobal/addTest/addNodeVisitor/addTokenParser/addExtension can now be called in any order) + * added Twig_Environment::display() + * made the escape filter smarter when the encoding is not supported by PHP + * added a convert_encoding filter + * moved all node manipulations outside the compile() Node method + * made several speed optimizations + +* 1.3.0 (2011-10-08) + +no changes + +* 1.3.0-RC1 (2011-10-04) + + * added an optimization for the parent() function + * added cache reloading when auto_reload is true and an extension has been modified + * added the possibility to force the escaping of a string already marked as safe (instance of Twig_Markup) + * allowed empty templates to be used as traits + * added traits support for the "parent" function + +* 1.2.0 (2011-09-13) + +no changes + +* 1.2.0-RC1 (2011-09-10) + + * enhanced the exception when a tag remains unclosed + * added support for empty Countable objects for the "empty" test + * fixed algorithm that determines if a template using inheritance is valid (no output between block definitions) + * added better support for encoding problems when escaping a string (available as of PHP 5.4) + * added a way to ignore a missing template when using the "include" tag ({% include "foo" ignore missing %}) + * added support for an array of templates to the "include" and "extends" tags ({% include ['foo', 'bar'] %}) + * added support for bitwise operators in expressions + * added the "attribute" function to allow getting dynamic attributes on variables + * added Twig_Loader_Chain + * added Twig_Loader_Array::setTemplate() + * added an optimization for the set tag when used to capture a large chunk of static text + * changed name regex to match PHP one "[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*" (works for blocks, tags, functions, filters, and macros) + * removed the possibility to use the "extends" tag from a block + * added "if" modifier support to "for" loops + +* 1.1.2 (2011-07-30) + + * fixed json_encode filter on PHP 5.2 + * fixed regression introduced in 1.1.1 ({{ block(foo|lower) }}) + * fixed inheritance when using conditional parents + * fixed compilation of templates when the body of a child template is not empty + * fixed output when a macro throws an exception + * fixed a parsing problem when a large chunk of text is enclosed in a comment tag + * added PHPDoc for all Token parsers and Core extension functions + +* 1.1.1 (2011-07-17) + + * added a performance optimization in the Optimizer (also helps to lower the number of nested level calls) + * made some performance improvement for some edge cases + +* 1.1.0 (2011-06-28) + + * fixed json_encode filter + +* 1.1.0-RC3 (2011-06-24) + + * fixed method case-sensitivity when using the sandbox mode + * added timezone support for the date filter + * fixed possible security problems with NUL bytes + +* 1.1.0-RC2 (2011-06-16) + + * added an exception when the template passed to "use" is not a string + * made 'a.b is defined' not throw an exception if a is not defined (in strict mode) + * added {% line \d+ %} directive + +* 1.1.0-RC1 (2011-05-28) + +Flush your cache after upgrading. + + * fixed date filter when using a timestamp + * fixed the defined test for some cases + * fixed a parsing problem when a large chunk of text is enclosed in a raw tag + * added support for horizontal reuse of template blocks (see docs for more information) + * added whitespace control modifier to all tags (see docs for more information) + * added null as an alias for none (the null test is also an alias for the none test now) + * made TRUE, FALSE, NONE equivalent to their lowercase counterparts + * wrapped all compilation and runtime exceptions with Twig_Error_Runtime and added logic to guess the template name and line + * moved display() method to Twig_Template (generated templates should now use doDisplay() instead) + +* 1.0.0 (2011-03-27) + + * fixed output when using mbstring + * fixed duplicate call of methods when using the sandbox + * made the charset configurable for the escape filter + +* 1.0.0-RC2 (2011-02-21) + + * changed the way {% set %} works when capturing (the content is now marked as safe) + * added support for macro name in the endmacro tag + * make Twig_Error compatible with PHP 5.3.0 > + * fixed an infinite loop on some Windows configurations + * fixed the "length" filter for numbers + * fixed Template::getAttribute() as properties in PHP are case sensitive + * removed coupling between Twig_Node and Twig_Template + * fixed the ternary operator precedence rule + +* 1.0.0-RC1 (2011-01-09) + +Backward incompatibilities: + + * the "items" filter, which has been deprecated for quite a long time now, has been removed + * the "range" filter has been converted to a function: 0|range(10) -> range(0, 10) + * the "constant" filter has been converted to a function: {{ some_date|date('DATE_W3C'|constant) }} -> {{ some_date|date(constant('DATE_W3C')) }} + * the "cycle" filter has been converted to a function: {{ ['odd', 'even']|cycle(i) }} -> {{ cycle(['odd', 'even'], i) }} + * the "for" tag does not support "joined by" anymore + * the "autoescape" first argument is now "true"/"false" (instead of "on"/"off") + * the "parent" tag has been replaced by a "parent" function ({{ parent() }} instead of {% parent %}) + * the "display" tag has been replaced by a "block" function ({{ block('title') }} instead of {% display title %}) + * removed the grammar and simple token parser (moved to the Twig Extensions repository) + +Changes: + + * added "needs_context" option for filters and functions (the context is then passed as a first argument) + * added global variables support + * made macros return their value instead of echoing directly (fixes calling a macro in sandbox mode) + * added the "from" tag to import macros as functions + * added support for functions (a function is just syntactic sugar for a getAttribute() call) + * made macros callable when sandbox mode is enabled + * added an exception when a macro uses a reserved name + * the "default" filter now uses the "empty" test instead of just checking for null + * added the "empty" test + +* 0.9.10 (2010-12-16) + +Backward incompatibilities: + + * The Escaper extension is enabled by default, which means that all displayed + variables are now automatically escaped. You can revert to the previous + behavior by removing the extension via $env->removeExtension('escaper') + or just set the 'autoescape' option to 'false'. + * removed the "without loop" attribute for the "for" tag (not needed anymore + as the Optimizer take care of that for most cases) + * arrays and hashes have now a different syntax + * arrays keep the same syntax with square brackets: [1, 2] + * hashes now use curly braces (["a": "b"] should now be written as {"a": "b"}) + * support for "arrays with keys" and "hashes without keys" is not supported anymore ([1, "foo": "bar"] or {"foo": "bar", 1}) + * the i18n extension is now part of the Twig Extensions repository + +Changes: + + * added the merge filter + * removed 'is_escaper' option for filters (a left over from the previous version) -- you must use 'is_safe' now instead + * fixed usage of operators as method names (like is, in, and not) + * changed the order of execution for node visitors + * fixed default() filter behavior when used with strict_variables set to on + * fixed filesystem loader compatibility with PHAR files + * enhanced error messages when an unexpected token is parsed in an expression + * fixed filename not being added to syntax error messages + * added the autoescape option to enable/disable autoescaping + * removed the newline after a comment (mimics PHP behavior) + * added a syntax error exception when parent block is used on a template that does not extend another one + * made the Escaper extension enabled by default + * fixed sandbox extension when used with auto output escaping + * fixed escaper when wrapping a Twig_Node_Print (the original class must be preserved) + * added an Optimizer extension (enabled by default; optimizes "for" loops and "raw" filters) + * added priority to node visitors + +* 0.9.9 (2010-11-28) + +Backward incompatibilities: + * the self special variable has been renamed to _self + * the odd and even filters are now tests: + {{ foo|odd }} must now be written {{ foo is odd }} + * the "safe" filter has been renamed to "raw" + * in Node classes, + sub-nodes are now accessed via getNode() (instead of property access) + attributes via getAttribute() (instead of array access) + * the urlencode filter had been renamed to url_encode + * the include tag now merges the passed variables with the current context by default + (the old behavior is still possible by adding the "only" keyword) + * moved Exceptions to Twig_Error_* (Twig_SyntaxError/Twig_RuntimeError are now Twig_Error_Syntax/Twig_Error_Runtime) + * removed support for {{ 1 < i < 3 }} (use {{ i > 1 and i < 3 }} instead) + * the "in" filter has been removed ({{ a|in(b) }} should now be written {{ a in b }}) + +Changes: + * added file and line to Twig_Error_Runtime exceptions thrown from Twig_Template + * changed trans tag to accept any variable for the plural count + * fixed sandbox mode (__toString() method check was not enforced if called implicitly from complex statements) + * added the ** (power) operator + * changed the algorithm used for parsing expressions + * added the spaceless tag + * removed trim_blocks option + * added support for is*() methods for attributes (foo.bar now looks for foo->getBar() or foo->isBar()) + * changed all exceptions to extend Twig_Error + * fixed unary expressions ({{ not(1 or 0) }}) + * fixed child templates (with an extend tag) that uses one or more imports + * added support for {{ 1 not in [2, 3] }} (more readable than the current {{ not (1 in [2, 3]) }}) + * escaping has been rewritten + * the implementation of template inheritance has been rewritten + (blocks can now be called individually and still work with inheritance) + * fixed error handling for if tag when a syntax error occurs within a subparse process + * added a way to implement custom logic for resolving token parsers given a tag name + * fixed js escaper to be stricter (now uses a whilelist-based js escaper) + * added the following filers: "constant", "trans", "replace", "json_encode" + * added a "constant" test + * fixed objects with __toString() not being autoescaped + * fixed subscript expressions when calling __call() (methods now keep the case) + * added "test" feature (accessible via the "is" operator) + * removed the debug tag (should be done in an extension) + * fixed trans tag when no vars are used in plural form + * fixed race condition when writing template cache + * added the special _charset variable to reference the current charset + * added the special _context variable to reference the current context + * renamed self to _self (to avoid conflict) + * fixed Twig_Template::getAttribute() for protected properties + +* 0.9.8 (2010-06-28) + +Backward incompatibilities: + * the trans tag plural count is now attached to the plural tag: + old: `{% trans count %}...{% plural %}...{% endtrans %}` + new: `{% trans %}...{% plural count %}...{% endtrans %}` + + * added a way to translate strings coming from a variable ({% trans var %}) + * fixed trans tag when used with the Escaper extension + * fixed default cache umask + * removed Twig_Template instances from the debug tag output + * fixed objects with __isset() defined + * fixed set tag when used with a capture + * fixed type hinting for Twig_Environment::addFilter() method + +* 0.9.7 (2010-06-12) + +Backward incompatibilities: + * changed 'as' to '=' for the set tag ({% set title as "Title" %} must now be {% set title = "Title" %}) + * removed the sandboxed attribute of the include tag (use the new sandbox tag instead) + * refactored the Node system (if you have custom nodes, you will have to update them to use the new API) + + * added self as a special variable that refers to the current template (useful for importing macros from the current template) + * added Twig_Template instance support to the include tag + * added support for dynamic and conditional inheritance ({% extends some_var %} and {% extends standalone ? "minimum" : "base" %}) + * added a grammar sub-framework to ease the creation of custom tags + * fixed the for tag for large arrays (some loop variables are now only available for arrays and objects that implement the Countable interface) + * removed the Twig_Resource::resolveMissingFilter() method + * fixed the filter tag which did not apply filtering to included files + * added a bunch of unit tests + * added a bunch of phpdoc + * added a sandbox tag in the sandbox extension + * changed the date filter to support any date format supported by DateTime + * added strict_variable setting to throw an exception when an invalid variable is used in a template (disabled by default) + * added the lexer, parser, and compiler as arguments to the Twig_Environment constructor + * changed the cache option to only accepts an explicit path to a cache directory or false + * added a way to add token parsers, filters, and visitors without creating an extension + * added three interfaces: Twig_NodeInterface, Twig_TokenParserInterface, and Twig_FilterInterface + * changed the generated code to match the new coding standards + * fixed sandbox mode (__toString() method check was not enforced if called implicitly from a simple statement like {{ article }}) + * added an exception when a child template has a non-empty body (as it is always ignored when rendering) + +* 0.9.6 (2010-05-12) + + * fixed variables defined outside a loop and for which the value changes in a for loop + * fixed the test suite for PHP 5.2 and older versions of PHPUnit + * added support for __call() in expression resolution + * fixed node visiting for macros (macros are now visited by visitors as any other node) + * fixed nested block definitions with a parent call (rarely useful but nonetheless supported now) + * added the cycle filter + * fixed the Lexer when mbstring.func_overload is used with an mbstring.internal_encoding different from ASCII + * added a long-syntax for the set tag ({% set foo %}...{% endset %}) + * unit tests are now powered by PHPUnit + * added support for gettext via the `i18n` extension + * fixed twig_capitalize_string_filter() and fixed twig_length_filter() when used with UTF-8 values + * added a more useful exception if an if tag is not closed properly + * added support for escaping strategy in the autoescape tag + * fixed lexer when a template has a big chunk of text between/in a block + +* 0.9.5 (2010-01-20) + +As for any new release, don't forget to remove all cached templates after +upgrading. + +If you have defined custom filters, you MUST upgrade them for this release. To +upgrade, replace "array" with "new Twig_Filter_Function", and replace the +environment constant by the "needs_environment" option: + + // before + 'even' => array('twig_is_even_filter', false), + 'escape' => array('twig_escape_filter', true), + + // after + 'even' => new Twig_Filter_Function('twig_is_even_filter'), + 'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true)), + +If you have created NodeTransformer classes, you will need to upgrade them to +the new interface (please note that the interface is not yet considered +stable). + + * fixed list nodes that did not extend the Twig_NodeListInterface + * added the "without loop" option to the for tag (it disables the generation of the loop variable) + * refactored node transformers to node visitors + * fixed automatic-escaping for blocks + * added a way to specify variables to pass to an included template + * changed the automatic-escaping rules to be more sensible and more configurable in custom filters (the documentation lists all the rules) + * improved the filter system to allow object methods to be used as filters + * changed the Array and String loaders to actually make use of the cache mechanism + * included the default filter function definitions in the extension class files directly (Core, Escaper) + * added the // operator (like the floor() PHP function) + * added the .. operator (as a syntactic sugar for the range filter when the step is 1) + * added the in operator (as a syntactic sugar for the in filter) + * added the following filters in the Core extension: in, range + * added support for arrays (same behavior as in PHP, a mix between lists and dictionaries, arrays and hashes) + * enhanced some error messages to provide better feedback in case of parsing errors + +* 0.9.4 (2009-12-02) + +If you have custom loaders, you MUST upgrade them for this release: The +Twig_Loader base class has been removed, and the Twig_LoaderInterface has also +been changed (see the source code for more information or the documentation). + + * added support for DateTime instances for the date filter + * fixed loop.last when the array only has one item + * made it possible to insert newlines in tag and variable blocks + * fixed a bug when a literal '\n' were present in a template text + * fixed bug when the filename of a template contains */ + * refactored loaders + +* 0.9.3 (2009-11-11) + +This release is NOT backward compatible with the previous releases. + + The loaders do not take the cache and autoReload arguments anymore. Instead, + the Twig_Environment class has two new options: cache and auto_reload. + Upgrading your code means changing this kind of code: + + $loader = new Twig_Loader_Filesystem('/path/to/templates', '/path/to/compilation_cache', true); + $twig = new Twig_Environment($loader); + + to something like this: + + $loader = new Twig_Loader_Filesystem('/path/to/templates'); + $twig = new Twig_Environment($loader, array( + 'cache' => '/path/to/compilation_cache', + 'auto_reload' => true, + )); + + * deprecated the "items" filter as it is not needed anymore + * made cache and auto_reload options of Twig_Environment instead of arguments of Twig_Loader + * optimized template loading speed + * removed output when an error occurs in a template and render() is used + * made major speed improvements for loops (up to 300% on even the smallest loops) + * added properties as part of the sandbox mode + * added public properties support (obj.item can now be the item property on the obj object) + * extended set tag to support expression as value ({% set foo as 'foo' ~ 'bar' %} ) + * fixed bug when \ was used in HTML + +* 0.9.2 (2009-10-29) + + * made some speed optimizations + * changed the cache extension to .php + * added a js escaping strategy + * added support for short block tag + * changed the filter tag to allow chained filters + * made lexer more flexible as you can now change the default delimiters + * added set tag + * changed default directory permission when cache dir does not exist (more secure) + * added macro support + * changed filters first optional argument to be a Twig_Environment instance instead of a Twig_Template instance + * made Twig_Autoloader::autoload() a static method + * avoid writing template file if an error occurs + * added $ escaping when outputting raw strings + * enhanced some error messages to ease debugging + * fixed empty cache files when the template contains an error + +* 0.9.1 (2009-10-14) + + * fixed a bug in PHP 5.2.6 + * fixed numbers with one than one decimal + * added support for method calls with arguments ({{ foo.bar('a', 43) }}) + * made small speed optimizations + * made minor tweaks to allow better extensibility and flexibility + +* 0.9.0 (2009-10-12) + + * Initial release diff --git a/vendor/twig/twig/LICENSE b/vendor/twig/twig/LICENSE new file mode 100644 index 000000000..a470002bf --- /dev/null +++ b/vendor/twig/twig/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2009-2014 by the Twig Team. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/twig/twig/README.rst b/vendor/twig/twig/README.rst new file mode 100644 index 000000000..81737b0b2 --- /dev/null +++ b/vendor/twig/twig/README.rst @@ -0,0 +1,15 @@ +Twig, the flexible, fast, and secure template language for PHP +============================================================== + +Twig is a template language for PHP, released under the new BSD license (code +and documentation). + +Twig uses a syntax similar to the Django and Jinja template languages which +inspired the Twig runtime environment. + +More Information +---------------- + +Read the `documentation`_ for more information. + +.. _documentation: http://twig.sensiolabs.org/documentation diff --git a/vendor/twig/twig/lib/Twig/Autoloader.php b/vendor/twig/twig/lib/Twig/Autoloader.php new file mode 100644 index 000000000..42f16f094 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Autoloader.php @@ -0,0 +1,48 @@ + + */ +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) + { + if (version_compare(phpversion(), '5.3.0', '>=')) { + spl_autoload_register(array(__CLASS__, 'autoload'), true, $prepend); + } else { + spl_autoload_register(array(__CLASS__, 'autoload')); + } + } + + /** + * 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/vendor/twig/twig/lib/Twig/Compiler.php b/vendor/twig/twig/lib/Twig/Compiler.php new file mode 100644 index 000000000..1796e40a7 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Compiler.php @@ -0,0 +1,270 @@ + + */ +class Twig_Compiler implements Twig_CompilerInterface +{ + protected $lastLine; + protected $source; + protected $indentation; + protected $env; + protected $debugInfo; + protected $sourceOffset; + protected $sourceLine; + protected $filename; + + /** + * Constructor. + * + * @param Twig_Environment $env The twig environment instance + */ + public function __construct(Twig_Environment $env) + { + $this->env = $env; + $this->debugInfo = array(); + } + + 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->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() + { + 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; + } +} diff --git a/vendor/twig/twig/lib/Twig/CompilerInterface.php b/vendor/twig/twig/lib/Twig/CompilerInterface.php new file mode 100644 index 000000000..272c7672f --- /dev/null +++ b/vendor/twig/twig/lib/Twig/CompilerInterface.php @@ -0,0 +1,36 @@ + + * + * @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/vendor/twig/twig/lib/Twig/Environment.php b/vendor/twig/twig/lib/Twig/Environment.php new file mode 100644 index 000000000..f1fec00fc --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Environment.php @@ -0,0 +1,1254 @@ + + */ +class Twig_Environment +{ + const VERSION = '1.16.1-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; + protected $extensionInitialized; + protected $loadedTemplates; + protected $strictVariables; + protected $unaryOperators; + protected $binaryOperators; + protected $templateClassPrefix = '__TwigTemplate_'; + protected $functionCallbacks; + protected $filterCallbacks; + protected $staging; + + /** + * 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, 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 + * * 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); + } + + $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->runtimeInitialized = false; + $this->setCache($options['cache']); + $this->functionCallbacks = array(); + $this->filterCallbacks = array(); + + $this->addExtension(new Twig_Extension_Core()); + $this->addExtension(new Twig_Extension_Escaper($options['autoescape'])); + $this->addExtension(new Twig_Extension_Optimizer($options['optimizations'])); + $this->extensionInitialized = false; + $this->staging = new Twig_Extension_Staging(); + } + + /** + * 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 cache directory or false if cache is disabled. + * + * @return string|false + */ + public function getCache() + { + return $this->cache; + } + + /** + * Sets the cache directory or false if cache is disabled. + * + * @param string|false $cache The absolute path to the compiled templates, + * or false to disable cache + */ + public function setCache($cache) + { + $this->cache = $cache ? $cache : false; + } + + /** + * 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 + */ + public function getCacheFilename($name) + { + if (false === $this->cache) { + return false; + } + + $class = substr($this->getTemplateClass($name), strlen($this->templateClassPrefix)); + + return $this->getCache().'/'.substr($class, 0, 2).'/'.substr($class, 2, 2).'/'.substr($class, 4).'.php'; + } + + /** + * Gets the template class associated with the given string. + * + * @param string $name The name for which to calculate the template class name + * @param int $index The index if it is an embedded template + * + * @return string The template class name + */ + public function getTemplateClass($name, $index = null) + { + return $this->templateClassPrefix.hash('sha256', $this->getLoader()->getCacheKey($name)).(null === $index ? '' : '_'.$index); + } + + /** + * Gets the template class prefix. + * + * @return string The template class prefix + */ + public function getTemplateClassPrefix() + { + 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 (false === $cache = $this->getCacheFilename($name)) { + eval('?>'.$this->compileSource($this->getLoader()->getSource($name), $name)); + } else { + if (!is_file($cache) || ($this->isAutoReload() && !$this->isTemplateFresh($name, filemtime($cache)))) { + $this->writeCacheFile($cache, $this->compileSource($this->getLoader()->getSource($name), $name)); + } + + require_once $cache; + } + } + + if (!$this->runtimeInitialized) { + $this->initRuntime(); + } + + return $this->loadedTemplates[$cls] = new $cls($this); + } + + /** + * 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 timestamp $time The last modification time of the cached template + * + * @return bool true if the template is fresh, false otherwise + */ + public function isTemplateFresh($name, $time) + { + foreach ($this->extensions as $extension) { + $r = new ReflectionObject($extension); + if (filemtime($r->getFileName()) > $time) { + return false; + } + } + + return $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. + */ + public function clearTemplateCache() + { + $this->loadedTemplates = array(); + } + + /** + * Clears the template cache files on the filesystem. + */ + public function clearCacheFiles() + { + if (false === $this->cache) { + return; + } + + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->cache), 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 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 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 { + return $this->compile($this->parse($this->tokenize($source, $name))); + } 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. + */ + public function initRuntime() + { + $this->runtimeInitialized = true; + + foreach ($this->getExtensions() as $extension) { + $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) + { + if ($this->extensionInitialized) { + throw new LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $extension->getName())); + } + + $this->extensions[$extension->getName()] = $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) + { + 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 + */ + 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 + */ + 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 + */ + 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(); + } + + 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 + */ + 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 registerUndefinedFunctionCallback. + * + * @return Twig_FilterInterface[] An array of Twig_FilterInterface instances + * + * @see registerUndefinedFilterCallback + */ + 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(); + } + + 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 + */ + 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 + */ + 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(); + } + + 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 + */ + 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 + */ + 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(); + } + + /* This condition must be uncommented in Twig 2.0 + if (!array_key_exists($name, $this->globals)) { + 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 + */ + 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 + */ + public function getUnaryOperators() + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + return $this->unaryOperators; + } + + /** + * Gets the registered binary Operators. + * + * @return array An array of binary operators + */ + public function getBinaryOperators() + { + if (!$this->extensionInitialized) { + $this->initExtensions(); + } + + return $this->binaryOperators; + } + + public 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); + } + + protected function initGlobals() + { + $globals = array(); + foreach ($this->extensions as $extension) { + $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); + } + + protected function initExtensions() + { + if ($this->extensionInitialized) { + return; + } + + $this->extensionInitialized = true; + $this->parsers = new Twig_TokenParserBroker(); + $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); + } + + protected function initExtension(Twig_ExtensionInterface $extension) + { + // filters + foreach ($extension->getFilters() as $name => $filter) { + if ($name instanceof Twig_SimpleFilter) { + $filter = $name; + $name = $filter->getName(); + } elseif ($filter instanceof Twig_SimpleFilter) { + $name = $filter->getName(); + } + + $this->filters[$name] = $filter; + } + + // functions + foreach ($extension->getFunctions() as $name => $function) { + if ($name instanceof Twig_SimpleFunction) { + $function = $name; + $name = $function->getName(); + } elseif ($function instanceof Twig_SimpleFunction) { + $name = $function->getName(); + } + + $this->functions[$name] = $function; + } + + // tests + foreach ($extension->getTests() as $name => $test) { + if ($name instanceof Twig_SimpleTest) { + $test = $name; + $name = $test->getName(); + } elseif ($test instanceof Twig_SimpleTest) { + $name = $test->getName(); + } + + $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) { + $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]); + } + } + + protected function writeCacheFile($file, $content) + { + $dir = dirname($file); + 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($file)); + if (false !== @file_put_contents($tmpFile, $content)) { + // rename does not work on Win32 before 5.2.6 + if (@rename($tmpFile, $file) || (@copy($tmpFile, $file) && unlink($tmpFile))) { + @chmod($file, 0666 & ~umask()); + + return; + } + } + + throw new RuntimeException(sprintf('Failed to write cache file "%s".', $file)); + } +} diff --git a/vendor/twig/twig/lib/Twig/Error.php b/vendor/twig/twig/lib/Twig/Error.php new file mode 100644 index 000000000..67728874f --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Error.php @@ -0,0 +1,248 @@ + + */ +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 (version_compare(PHP_VERSION, '5.3.0', '<')) { + $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)); + } + + protected function updateRepr() + { + $this->message = $this->rawMessage; + + $dot = false; + if ('.' === substr($this->message, -1)) { + $this->message = substr($this->message, 0, -1); + $dot = 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 .= '.'; + } + } + + protected function guessTemplateInfo() + { + $template = null; + $templateClass = null; + + if (version_compare(phpversion(), '5.3.6', '>=')) { + $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(); + 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/vendor/twig/twig/lib/Twig/Error/Loader.php b/vendor/twig/twig/lib/Twig/Error/Loader.php new file mode 100644 index 000000000..68efb574a --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Error/Loader.php @@ -0,0 +1,31 @@ + + */ +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/vendor/twig/twig/lib/Twig/Error/Runtime.php b/vendor/twig/twig/lib/Twig/Error/Runtime.php new file mode 100644 index 000000000..8b6ceddb9 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Error/Runtime.php @@ -0,0 +1,20 @@ + + */ +class Twig_Error_Runtime extends Twig_Error +{ +} diff --git a/vendor/twig/twig/lib/Twig/Error/Syntax.php b/vendor/twig/twig/lib/Twig/Error/Syntax.php new file mode 100644 index 000000000..0f5c57928 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Error/Syntax.php @@ -0,0 +1,20 @@ + + */ +class Twig_Error_Syntax extends Twig_Error +{ +} diff --git a/vendor/twig/twig/lib/Twig/ExistsLoaderInterface.php b/vendor/twig/twig/lib/Twig/ExistsLoaderInterface.php new file mode 100644 index 000000000..48df9e1a3 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/ExistsLoaderInterface.php @@ -0,0 +1,29 @@ + + * + * @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/vendor/twig/twig/lib/Twig/ExpressionParser.php b/vendor/twig/twig/lib/Twig/ExpressionParser.php new file mode 100644 index 000000000..01594f710 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/ExpressionParser.php @@ -0,0 +1,598 @@ + + */ +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; + } + + 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': + $args = $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) : new Twig_Node_Expression_Array(array(), $line), 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_TemplateInterface::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()); + } + + $node = new Twig_Node_Expression_MethodCall($node, 'get'.$arg->getAttribute('value'), $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 + */ + 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'); + if (in_array($token->getValue(), array('true', 'false', 'none'))) { + throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine(), $this->parser->getFilename()); + } + $targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $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)) { + $message = sprintf('The function "%s" does not exist', $name); + if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFunctions()))) { + $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); + } + + throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename()); + } + + 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)) { + $message = sprintf('The filter "%s" does not exist', $name); + if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFilters()))) { + $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); + } + + throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename()); + } + + 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)) { + return false; + } + + foreach ($node as $n) { + if (!$this->checkConstantExpression($n)) { + return false; + } + } + + return true; + } +} diff --git a/vendor/twig/twig/lib/Twig/Extension.php b/vendor/twig/twig/lib/Twig/Extension.php new file mode 100644 index 000000000..5c8ad5c96 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Extension.php @@ -0,0 +1,93 @@ +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; + } + + /** + * Returns the token parser instance to add to the existing list. + * + * @return Twig_TokenParser[] An array of Twig_TokenParser instances + */ + 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(), + ); + } + + /** + * Returns a list of filters to add to the existing list. + * + * @return array An array of filters + */ + 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', 'strtr'), + 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'), + 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; + } + + /** + * Returns a list of global functions to add to the existing list. + * + * @return array An array of global functions + */ + 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'))), + ); + } + + /** + * Returns a list of tests to add to the existing list. + * + * @return array An array of tests + */ + 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')), + 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')), + 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'), + ); + } + + /** + * Returns a list of operators to add to the existing list. + * + * @return array An array of operators + */ + 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), + ), + ); + } + + 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(); + $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); + $class = $this->getTestNodeClass($parser, $name, $node->getLine()); + $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 getTestNodeClass(Twig_Parser $parser, $name, $line) + { + $env = $parser->getEnvironment(); + $testMap = $env->getTests(); + $testName = null; + if (isset($testMap[$name])) { + $testName = $name; + } elseif ($parser->getStream()->test(Twig_Token::NAME_TYPE)) { + // try 2-words tests + $name = $name.' '.$parser->getCurrentToken()->getValue(); + + if (isset($testMap[$name])) { + $parser->getStream()->next(); + + $testName = $name; + } + } + + if (null === $testName) { + $message = sprintf('The test "%s" does not exist', $name); + if ($alternatives = $env->computeAlternatives($name, array_keys($env->getTests()))) { + $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); + } + + throw new Twig_Error_Syntax($message, $line, $parser->getFilename()); + } + + if ($testMap[$name] instanceof Twig_SimpleTest) { + return $testMap[$name]->getNodeClass(); + } + + return $testMap[$name] instanceof Twig_Test_Node ? $testMap[$name]->getClass() : 'Twig_Node_Expression_Test'; + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + 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('/(? $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. + * + *
+ *   {{ post.published_at|date("m/d/Y") }}
+ * 
+ * + * @param Twig_Environment $env A Twig_Environment instance + * @param DateTime|DateInterval|string $date A date + * @param string $format A format + * @param DateTimeZone|string $timezone A timezone + * + * @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 + * + *
+ *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
+ * 
+ * + * @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); + $date->modify($modifier); + + return $date; +} + +/** + * Converts an input to a DateTime instance. + * + *
+ *    {% if date(user.created_at) < date('+2days') %}
+ *      {# do something #}
+ *    {% endif %}
+ * 
+ * + * @param Twig_Environment $env A Twig_Environment instance + * @param DateTime|string $date A date + * @param DateTimeZone|string $timezone A timezone + * + * @return DateTime A DateTime instance + */ +function twig_date_converter(Twig_Environment $env, $date = null, $timezone = null) +{ + // determine the timezone + if (!$timezone) { + $defaultTimezone = $env->getExtension('core')->getTimezone(); + } elseif (!$timezone instanceof DateTimeZone) { + $defaultTimezone = new DateTimeZone($timezone); + } else { + $defaultTimezone = $timezone; + } + + // immutable dates + if ($date instanceof DateTimeImmutable) { + return false !== $timezone ? $date->setTimezone($defaultTimezone) : $date; + } + + if ($date instanceof DateTime || $date instanceof DateTimeInterface) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($defaultTimezone); + } + + return $date; + } + + $asString = (string) $date; + if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = '@'.$date; + } + + $date = new DateTime($date, $defaultTimezone); + if (false !== $timezone) { + $date->setTimezone($defaultTimezone); + } + + return $date; +} + +/** + * 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 (version_compare(PHP_VERSION, '5.3.0', '<')) { + /** + * 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. + * + *
+ *  {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
+ *
+ *  {% set items = items|merge({ 'peugeot': 'car' }) %}
+ *
+ *  {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #}
+ * 
+ * + * @param array $arr1 An array + * @param array $arr2 An array + * + * @return array The merged array + */ +function twig_array_merge($arr1, $arr2) +{ + if (!is_array($arr1) || !is_array($arr2)) { + throw new Twig_Error_Runtime(sprintf('The merge filter only works with arrays or hashes; %s and %s given.', gettype($arr1), 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) { + $item = iterator_to_array($item, false); + } + + 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 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. + * + *
+ *  {{ [1, 2, 3]|join('|') }}
+ *  {# returns 1|2|3 #}
+ *
+ *  {{ [1, 2, 3]|join }}
+ *  {# returns 123 #}
+ * 
+ * + * @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. + * + *
+ *  {{ "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] #}
+ * 
+ * + * @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($value, $delimiter, $limit = null) +{ + if (empty($delimiter)) { + return str_split($value, null === $limit ? 1 : $limit); + } + + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); +} + +// 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. +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: + * + *
+ *  {% for key in array|keys %}
+ *      {# ... #}
+ *  {% endfor %}
+ * 
+ * + * @param array $array An array + * + * @return array The keys + */ +function twig_get_array_keys_filter($array) +{ + if (is_object($array) && $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 (is_object($item) && $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 $array An array + */ +function twig_sort_filter($array) +{ + asort($array); + + return $array; +} + +/* used internally */ +function twig_in_filter($value, $compare) +{ + if (is_array($compare)) { + return in_array($value, $compare, is_object($value)); + } elseif (is_string($compare)) { + if (!strlen($value)) { + return empty($compare); + } + + return false !== strpos($compare, (string) $value); + } elseif ($compare instanceof Traversable) { + return in_array($value, iterator_to_array($compare, false), is_object($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) + */ +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; + } else { + 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 ('hiphop' === substr(PHP_VERSION, -6)) { + $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': + // hackish test to avoid version_compare that is much slower, this works unless PHP releases a 5.10.* + // at that point however PHP 5.2.* support can be removed + if (PHP_VERSION < '5.3.0') { + 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)); + } +} + +/* used internally */ +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)); + } +} + +/* used internally */ +function twig_ensure_traversable($seq) +{ + if ($seq instanceof Traversable || is_array($seq)) { + return $seq; + } + + return array(); +} + +/** + * Checks if a variable is empty. + * + *
+ * {# evaluates to true if the foo variable is null, false, or the empty string #}
+ * {% if foo is empty %}
+ *     {# ... #}
+ * {% endif %}
+ * 
+ * + * @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. + * + *
+ * {# evaluates to true if the foo variable is an array or a traversable object #}
+ * {% if foo is traversable %}
+ *     {# ... #}
+ * {% endif %}
+ * 
+ * + * @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 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 $with_context Whether to pass the current context variables or not + * @param bool $ignore_missing 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(); + } + } + + try { + return $env->resolveTemplate($template)->render($variables); + } catch (Twig_Error_Loader $e) { + if (!$ignoreMissing) { + throw $e; + } + } + + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); + } +} + +/** + * Returns a template content without rendering it. + * + * @param string $name The template name + * + * @return string The template source + */ +function twig_source(Twig_Environment $env, $name) +{ + return $env->getLoader()->getSource($name); +} + +/** + * 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) { + $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/vendor/twig/twig/lib/Twig/Extension/Debug.php b/vendor/twig/twig/lib/Twig/Extension/Debug.php new file mode 100644 index 000000000..e3a85bfe8 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Extension/Debug.php @@ -0,0 +1,71 @@ + $isDumpOutputHtmlSafe ? array('html') : array(), 'needs_context' => true, 'needs_environment' => true)), + ); + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + 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/vendor/twig/twig/lib/Twig/Extension/Escaper.php b/vendor/twig/twig/lib/Twig/Extension/Escaper.php new file mode 100644 index 000000000..d3e5ad0ec --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Extension/Escaper.php @@ -0,0 +1,107 @@ +setDefaultStrategy($defaultStrategy); + } + + /** + * Returns the token parser instances to add to the existing list. + * + * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances + */ + public function getTokenParsers() + { + return array(new Twig_TokenParser_AutoEscape()); + } + + /** + * Returns the node visitor instances to add to the existing list. + * + * @return Twig_NodeVisitorInterface[] An array of Twig_NodeVisitorInterface instances + */ + public function getNodeVisitors() + { + return array(new Twig_NodeVisitor_Escaper()); + } + + /** + * Returns a list of filters to add to the existing list. + * + * @return array An array of filters + */ + 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 mixed $defaultStrategy An escaping strategy + */ + public function setDefaultStrategy($defaultStrategy) + { + // for BC + if (true === $defaultStrategy) { + $defaultStrategy = 'html'; + } + + $this->defaultStrategy = $defaultStrategy; + } + + /** + * Gets the default strategy to use when not defined by the user. + * + * @param string $filename The template "filename" + * + * @return string 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) && is_callable($this->defaultStrategy)) { + return call_user_func($this->defaultStrategy, $filename); + } + + return $this->defaultStrategy; + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return 'escaper'; + } +} + +/** + * Marks a variable as being safe. + * + * @param string $string A PHP variable + */ +function twig_raw_filter($string) +{ + return $string; +} diff --git a/vendor/twig/twig/lib/Twig/Extension/Optimizer.php b/vendor/twig/twig/lib/Twig/Extension/Optimizer.php new file mode 100644 index 000000000..013fcb625 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Extension/Optimizer.php @@ -0,0 +1,35 @@ +optimizers = $optimizers; + } + + /** + * {@inheritdoc} + */ + public function getNodeVisitors() + { + return array(new Twig_NodeVisitor_Optimizer($this->optimizers)); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'optimizer'; + } +} diff --git a/vendor/twig/twig/lib/Twig/Extension/Sandbox.php b/vendor/twig/twig/lib/Twig/Extension/Sandbox.php new file mode 100644 index 000000000..c58259c6a --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Extension/Sandbox.php @@ -0,0 +1,112 @@ +policy = $policy; + $this->sandboxedGlobally = $sandboxed; + } + + /** + * Returns the token parser instances to add to the existing list. + * + * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances + */ + public function getTokenParsers() + { + return array(new Twig_TokenParser_Sandbox()); + } + + /** + * Returns the node visitor instances to add to the existing list. + * + * @return Twig_NodeVisitorInterface[] An array of Twig_NodeVisitorInterface instances + */ + 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; + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return 'sandbox'; + } +} diff --git a/vendor/twig/twig/lib/Twig/Extension/Staging.php b/vendor/twig/twig/lib/Twig/Extension/Staging.php new file mode 100644 index 000000000..8ab0f4596 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Extension/Staging.php @@ -0,0 +1,113 @@ + + */ +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; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + return $this->functions; + } + + public function addFilter($name, $filter) + { + $this->filters[$name] = $filter; + } + + /** + * {@inheritdoc} + */ + public function getFilters() + { + return $this->filters; + } + + public function addNodeVisitor(Twig_NodeVisitorInterface $visitor) + { + $this->visitors[] = $visitor; + } + + /** + * {@inheritdoc} + */ + public function getNodeVisitors() + { + return $this->visitors; + } + + public function addTokenParser(Twig_TokenParserInterface $parser) + { + $this->tokenParsers[] = $parser; + } + + /** + * {@inheritdoc} + */ + public function getTokenParsers() + { + return $this->tokenParsers; + } + + public function addGlobal($name, $value) + { + $this->globals[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function getGlobals() + { + return $this->globals; + } + + public function addTest($name, $test) + { + $this->tests[$name] = $test; + } + + /** + * {@inheritdoc} + */ + public function getTests() + { + return $this->tests; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'staging'; + } +} diff --git a/vendor/twig/twig/lib/Twig/Extension/StringLoader.php b/vendor/twig/twig/lib/Twig/Extension/StringLoader.php new file mode 100644 index 000000000..5e1a60d0a --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Extension/StringLoader.php @@ -0,0 +1,64 @@ + true)), + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'string_loader'; + } +} + +/** + * Loads a template from a string. + * + *
+ * {{ include(template_from_string("Hello {{ name }}")) }}
+ * 
+ * + * @param Twig_Environment $env A Twig_Environment instance + * @param string $template A template as a string + * + * @return Twig_Template A Twig_Template instance + */ +function twig_template_from_string(Twig_Environment $env, $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 = $env->getLoader(), + )); + + $env->setLoader($loader); + try { + $template = $env->loadTemplate($name); + } catch (Exception $e) { + $env->setLoader($current); + + throw $e; + } + $env->setLoader($current); + + return $template; +} diff --git a/vendor/twig/twig/lib/Twig/ExtensionInterface.php b/vendor/twig/twig/lib/Twig/ExtensionInterface.php new file mode 100644 index 000000000..49541b02e --- /dev/null +++ b/vendor/twig/twig/lib/Twig/ExtensionInterface.php @@ -0,0 +1,83 @@ + + */ +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 + */ + public function initRuntime(Twig_Environment $environment); + + /** + * Returns the token parser instances to add to the existing list. + * + * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances + */ + 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 array An array of filters + */ + public function getFilters(); + + /** + * Returns a list of tests to add to the existing list. + * + * @return array An array of tests + */ + public function getTests(); + + /** + * Returns a list of functions to add to the existing list. + * + * @return array An array of functions + */ + 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 + */ + public function getGlobals(); + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName(); +} diff --git a/vendor/twig/twig/lib/Twig/Filter.php b/vendor/twig/twig/lib/Twig/Filter.php new file mode 100644 index 000000000..5cfbb6625 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Filter.php @@ -0,0 +1,81 @@ + + * @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/vendor/twig/twig/lib/Twig/Filter/Function.php b/vendor/twig/twig/lib/Twig/Filter/Function.php new file mode 100644 index 000000000..ad374a551 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Filter/Function.php @@ -0,0 +1,37 @@ + + * @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/vendor/twig/twig/lib/Twig/Filter/Method.php b/vendor/twig/twig/lib/Twig/Filter/Method.php new file mode 100644 index 000000000..63c8c3be4 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Filter/Method.php @@ -0,0 +1,39 @@ + + * @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/vendor/twig/twig/lib/Twig/Filter/Node.php b/vendor/twig/twig/lib/Twig/Filter/Node.php new file mode 100644 index 000000000..8744c5e00 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Filter/Node.php @@ -0,0 +1,39 @@ + + * @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/vendor/twig/twig/lib/Twig/FilterCallableInterface.php b/vendor/twig/twig/lib/Twig/FilterCallableInterface.php new file mode 100644 index 000000000..145534dfd --- /dev/null +++ b/vendor/twig/twig/lib/Twig/FilterCallableInterface.php @@ -0,0 +1,23 @@ + + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_FilterCallableInterface +{ + public function getCallable(); +} diff --git a/vendor/twig/twig/lib/Twig/FilterInterface.php b/vendor/twig/twig/lib/Twig/FilterInterface.php new file mode 100644 index 000000000..5319ecc9f --- /dev/null +++ b/vendor/twig/twig/lib/Twig/FilterInterface.php @@ -0,0 +1,42 @@ + + * @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/vendor/twig/twig/lib/Twig/Function.php b/vendor/twig/twig/lib/Twig/Function.php new file mode 100644 index 000000000..b5ffb2b04 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Function.php @@ -0,0 +1,71 @@ + + * @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/vendor/twig/twig/lib/Twig/Function/Function.php b/vendor/twig/twig/lib/Twig/Function/Function.php new file mode 100644 index 000000000..d1e1b96a2 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Function/Function.php @@ -0,0 +1,38 @@ + + * @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/vendor/twig/twig/lib/Twig/Function/Method.php b/vendor/twig/twig/lib/Twig/Function/Method.php new file mode 100644 index 000000000..67039a956 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Function/Method.php @@ -0,0 +1,40 @@ + + * @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/vendor/twig/twig/lib/Twig/Function/Node.php b/vendor/twig/twig/lib/Twig/Function/Node.php new file mode 100644 index 000000000..06a0d0dbe --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Function/Node.php @@ -0,0 +1,39 @@ + + * @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/vendor/twig/twig/lib/Twig/FunctionCallableInterface.php b/vendor/twig/twig/lib/Twig/FunctionCallableInterface.php new file mode 100644 index 000000000..0aab4f5ec --- /dev/null +++ b/vendor/twig/twig/lib/Twig/FunctionCallableInterface.php @@ -0,0 +1,23 @@ + + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_FunctionCallableInterface +{ + public function getCallable(); +} diff --git a/vendor/twig/twig/lib/Twig/FunctionInterface.php b/vendor/twig/twig/lib/Twig/FunctionInterface.php new file mode 100644 index 000000000..67f4f89c0 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/FunctionInterface.php @@ -0,0 +1,39 @@ + + * @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/vendor/twig/twig/lib/Twig/Lexer.php b/vendor/twig/twig/lib/Twig/Lexer.php new file mode 100644 index 000000000..ad3ec7d48 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Lexer.php @@ -0,0 +1,409 @@ + + */ +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 (!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/vendor/twig/twig/lib/Twig/LexerInterface.php b/vendor/twig/twig/lib/Twig/LexerInterface.php new file mode 100644 index 000000000..24a947870 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/LexerInterface.php @@ -0,0 +1,32 @@ + + * + * @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/vendor/twig/twig/lib/Twig/Loader/Array.php b/vendor/twig/twig/lib/Twig/Loader/Array.php new file mode 100644 index 000000000..ac5610485 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Loader/Array.php @@ -0,0 +1,95 @@ + + */ +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) + * + * @see Twig_Loader + */ + 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/vendor/twig/twig/lib/Twig/Loader/Chain.php b/vendor/twig/twig/lib/Twig/Loader/Chain.php new file mode 100644 index 000000000..7919eda62 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Loader/Chain.php @@ -0,0 +1,138 @@ + + */ +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, 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, 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, implode(' ', $exceptions))); + } +} diff --git a/vendor/twig/twig/lib/Twig/Loader/Filesystem.php b/vendor/twig/twig/lib/Twig/Loader/Filesystem.php new file mode 100644 index 000000000..d0ae1cc1d --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Loader/Filesystem.php @@ -0,0 +1,236 @@ + + */ +class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderInterface +{ + /** Identifier of the main namespace. */ + const MAIN_NAMESPACE = '__main__'; + + protected $paths = array(); + protected $cache = 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 = 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 = 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 { + $this->findTemplate($name); + + return true; + } catch (Twig_Error_Loader $exception) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function isFresh($name, $time) + { + return filemtime($this->findTemplate($name)) <= $time; + } + + protected function findTemplate($name) + { + $name = $this->normalizeName($name); + + if (isset($this->cache[$name])) { + return $this->cache[$name]; + } + + $this->validateName($name); + + list($namespace, $shortname) = $this->parseName($name); + + if (!isset($this->paths[$namespace])) { + throw new Twig_Error_Loader(sprintf('There are no registered paths for namespace "%s".', $namespace)); + } + + foreach ($this->paths[$namespace] as $path) { + if (is_file($path.'/'.$shortname)) { + return $this->cache[$name] = $path.'/'.$shortname; + } + } + + throw new Twig_Error_Loader(sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]))); + } + + 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,}#', '/', strtr((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/vendor/twig/twig/lib/Twig/Loader/String.php b/vendor/twig/twig/lib/Twig/Loader/String.php new file mode 100644 index 000000000..8ad9856cc --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Loader/String.php @@ -0,0 +1,59 @@ + + */ +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/vendor/twig/twig/lib/Twig/LoaderInterface.php b/vendor/twig/twig/lib/Twig/LoaderInterface.php new file mode 100644 index 000000000..4405291ff --- /dev/null +++ b/vendor/twig/twig/lib/Twig/LoaderInterface.php @@ -0,0 +1,52 @@ + + */ +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 timestamp $time 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/vendor/twig/twig/lib/Twig/Markup.php b/vendor/twig/twig/lib/Twig/Markup.php new file mode 100644 index 000000000..69871fcbd --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Markup.php @@ -0,0 +1,37 @@ + + */ +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/vendor/twig/twig/lib/Twig/Node.php b/vendor/twig/twig/lib/Twig/Node.php new file mode 100644 index 000000000..978e76628 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node.php @@ -0,0 +1,226 @@ + + */ +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); + } + + public function toXml($asDom = false) + { + $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 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. + * + * @param string The attribute name + * + * @return mixed The attribute value + */ + 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. + * + * @param string The attribute name + * @param mixed The attribute value + */ + public function setAttribute($name, $value) + { + $this->attributes[$name] = $value; + } + + /** + * Removes an attribute. + * + * @param string The attribute name + */ + public function removeAttribute($name) + { + unset($this->attributes[$name]); + } + + /** + * Returns true if the node with the given identifier exists. + * + * @param string The node name + * + * @return bool true if the node with the given name exists, false otherwise + */ + public function hasNode($name) + { + return array_key_exists($name, $this->nodes); + } + + /** + * Gets a node by name. + * + * @param string The node name + * + * @return Twig_Node A Twig_Node instance + */ + 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 The node name + * @param Twig_Node A Twig_Node instance + */ + public function setNode($name, $node = null) + { + $this->nodes[$name] = $node; + } + + /** + * Removes a node by name. + * + * @param string The node 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/vendor/twig/twig/lib/Twig/Node/AutoEscape.php b/vendor/twig/twig/lib/Twig/Node/AutoEscape.php new file mode 100644 index 000000000..fcabf9033 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/AutoEscape.php @@ -0,0 +1,39 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler) + { + $compiler->subcompile($this->getNode('body')); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Block.php b/vendor/twig/twig/lib/Twig/Node/Block.php new file mode 100644 index 000000000..989e4a0ca --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Block.php @@ -0,0 +1,44 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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/vendor/twig/twig/lib/Twig/Node/BlockReference.php b/vendor/twig/twig/lib/Twig/Node/BlockReference.php new file mode 100644 index 000000000..a05ea045b --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/BlockReference.php @@ -0,0 +1,37 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) + ; + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Body.php b/vendor/twig/twig/lib/Twig/Node/Body.php new file mode 100644 index 000000000..3ffb1342b --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Body.php @@ -0,0 +1,19 @@ + + */ +class Twig_Node_Body extends Twig_Node +{ +} diff --git a/vendor/twig/twig/lib/Twig/Node/Do.php b/vendor/twig/twig/lib/Twig/Node/Do.php new file mode 100644 index 000000000..9981bc16b --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Do.php @@ -0,0 +1,38 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('') + ->subcompile($this->getNode('expr')) + ->raw(";\n") + ; + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Embed.php b/vendor/twig/twig/lib/Twig/Node/Embed.php new file mode 100644 index 000000000..4c9456dc0 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Embed.php @@ -0,0 +1,38 @@ + + */ +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->env->loadTemplate(") + ->string($this->getAttribute('filename')) + ->raw(', ') + ->string($this->getAttribute('index')) + ->raw(")") + ; + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression.php b/vendor/twig/twig/lib/Twig/Node/Expression.php new file mode 100644 index 000000000..a7382e7d6 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression.php @@ -0,0 +1,20 @@ + + */ +abstract class Twig_Node_Expression extends Twig_Node +{ +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Array.php b/vendor/twig/twig/lib/Twig/Node/Expression/Array.php new file mode 100644 index 000000000..6cf7ca144 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Array.php @@ -0,0 +1,86 @@ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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/vendor/twig/twig/lib/Twig/Node/Expression/AssignName.php b/vendor/twig/twig/lib/Twig/Node/Expression/AssignName.php new file mode 100644 index 000000000..4d5dbdb90 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/AssignName.php @@ -0,0 +1,28 @@ +raw('$context[') + ->string($this->getAttribute('name')) + ->raw(']') + ; + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary.php new file mode 100644 index 000000000..5c383d155 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary.php @@ -0,0 +1,40 @@ + $left, 'right' => $right), array(), $lineno); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Add.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Add.php new file mode 100644 index 000000000..0ef8e1172 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Add.php @@ -0,0 +1,18 @@ +raw('+'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/And.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/And.php new file mode 100644 index 000000000..d5752ebba --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/And.php @@ -0,0 +1,18 @@ +raw('&&'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseAnd.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseAnd.php new file mode 100644 index 000000000..9a46d8455 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseAnd.php @@ -0,0 +1,18 @@ +raw('&'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseOr.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseOr.php new file mode 100644 index 000000000..058a20bf6 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseOr.php @@ -0,0 +1,18 @@ +raw('|'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseXor.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseXor.php new file mode 100644 index 000000000..f4da73d44 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseXor.php @@ -0,0 +1,18 @@ +raw('^'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Concat.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Concat.php new file mode 100644 index 000000000..f9a646270 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Concat.php @@ -0,0 +1,18 @@ +raw('.'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Div.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Div.php new file mode 100644 index 000000000..e0797a612 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Div.php @@ -0,0 +1,18 @@ +raw('/'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/EndsWith.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/EndsWith.php new file mode 100644 index 000000000..5de6c72d2 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/EndsWith.php @@ -0,0 +1,30 @@ +raw('(0 === substr_compare(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(', -strlen(') + ->subcompile($this->getNode('right')) + ->raw(')))') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw(''); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Equal.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Equal.php new file mode 100644 index 000000000..7b1236d0a --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Equal.php @@ -0,0 +1,17 @@ +raw('=='); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/FloorDiv.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/FloorDiv.php new file mode 100644 index 000000000..d3518b558 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/FloorDiv.php @@ -0,0 +1,29 @@ +raw('intval(floor('); + parent::compile($compiler); + $compiler->raw('))'); + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('/'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Greater.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Greater.php new file mode 100644 index 000000000..a110bd92d --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Greater.php @@ -0,0 +1,17 @@ +raw('>'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/GreaterEqual.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/GreaterEqual.php new file mode 100644 index 000000000..3754fed21 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/GreaterEqual.php @@ -0,0 +1,17 @@ +raw('>='); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/In.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/In.php new file mode 100644 index 000000000..1d485b61c --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/In.php @@ -0,0 +1,33 @@ +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/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Less.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Less.php new file mode 100644 index 000000000..45fd30049 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Less.php @@ -0,0 +1,17 @@ +raw('<'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/LessEqual.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/LessEqual.php new file mode 100644 index 000000000..e38e257c3 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/LessEqual.php @@ -0,0 +1,17 @@ +raw('<='); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Matches.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Matches.php new file mode 100644 index 000000000..93bb29205 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Matches.php @@ -0,0 +1,28 @@ +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/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mod.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mod.php new file mode 100644 index 000000000..9924114fb --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mod.php @@ -0,0 +1,18 @@ +raw('%'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mul.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mul.php new file mode 100644 index 000000000..c91529ca6 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mul.php @@ -0,0 +1,18 @@ +raw('*'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotEqual.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotEqual.php new file mode 100644 index 000000000..26867ba20 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotEqual.php @@ -0,0 +1,17 @@ +raw('!='); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotIn.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotIn.php new file mode 100644 index 000000000..8f215f1c1 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotIn.php @@ -0,0 +1,33 @@ +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/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Or.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Or.php new file mode 100644 index 000000000..adba49c6f --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Or.php @@ -0,0 +1,18 @@ +raw('||'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Power.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Power.php new file mode 100644 index 000000000..6cd3a217c --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Power.php @@ -0,0 +1,33 @@ +raw('pow(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('**'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Range.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Range.php new file mode 100644 index 000000000..fc102fed3 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Range.php @@ -0,0 +1,33 @@ +raw('range(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw('..'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/StartsWith.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/StartsWith.php new file mode 100644 index 000000000..eb8c107cb --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/StartsWith.php @@ -0,0 +1,28 @@ +raw('(0 === strpos(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Twig_Compiler $compiler) + { + return $compiler->raw(''); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Sub.php b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Sub.php new file mode 100644 index 000000000..d44639915 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Binary/Sub.php @@ -0,0 +1,18 @@ +raw('-'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/BlockReference.php b/vendor/twig/twig/lib/Twig/Node/Expression/BlockReference.php new file mode 100644 index 000000000..4ddb2cf44 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/BlockReference.php @@ -0,0 +1,51 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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/vendor/twig/twig/lib/Twig/Node/Expression/Call.php b/vendor/twig/twig/lib/Twig/Node/Expression/Call.php new file mode 100644 index 000000000..912b837a9 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Call.php @@ -0,0 +1,176 @@ +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) + { + $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".', $this->getAttribute('type'), $this->getAttribute('name'))); + } + + $parameters[$name] = $node; + } + + if (!$named) { + return $parameters; + } + + if (!$callable) { + throw new LogicException(sprintf('Named arguments are not supported for %s "%s".', $this->getAttribute('type'), $this->getAttribute('name'))); + } + + // manage named arguments + 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'); + } else { + $r = new ReflectionFunction($callable); + } + + $definition = $r->getParameters(); + if ($this->hasNode('node')) { + array_shift($definition); + } + if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + array_shift($definition); + } + if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + array_shift($definition); + } + if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) { + foreach ($this->getAttribute('arguments') as $argument) { + array_shift($definition); + } + } + + $arguments = array(); + $pos = 0; + foreach ($definition as $param) { + $name = $this->normalizeName($param->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, $this->getAttribute('type'), $this->getAttribute('name'))); + } + + $arguments[] = $parameters[$name]; + unset($parameters[$name]); + } elseif (array_key_exists($pos, $parameters)) { + $arguments[] = $parameters[$pos]; + unset($parameters[$pos]); + ++$pos; + } elseif ($param->isDefaultValueAvailable()) { + $arguments[] = new Twig_Node_Expression_Constant($param->getDefaultValue(), -1); + } elseif ($param->isOptional()) { + break; + } else { + throw new Twig_Error_Syntax(sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name'))); + } + } + + if (!empty($parameters)) { + throw new Twig_Error_Syntax(sprintf('Unknown argument%s "%s" for %s "%s".', count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $this->getAttribute('type'), $this->getAttribute('name'))); + } + + 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)); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Conditional.php b/vendor/twig/twig/lib/Twig/Node/Expression/Conditional.php new file mode 100644 index 000000000..edcb1e2d2 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Conditional.php @@ -0,0 +1,31 @@ + $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/vendor/twig/twig/lib/Twig/Node/Expression/Constant.php b/vendor/twig/twig/lib/Twig/Node/Expression/Constant.php new file mode 100644 index 000000000..a91dc6985 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Constant.php @@ -0,0 +1,23 @@ + $value), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->repr($this->getAttribute('value')); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/ExtensionReference.php b/vendor/twig/twig/lib/Twig/Node/Expression/ExtensionReference.php new file mode 100644 index 000000000..db06abb0a --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/ExtensionReference.php @@ -0,0 +1,33 @@ + + */ +class Twig_Node_Expression_ExtensionReference extends Twig_Node_Expression +{ + public function __construct($name, $lineno, $tag = null) + { + parent::__construct(array(), array('name' => $name), $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler) + { + $compiler->raw(sprintf("\$this->env->getExtension('%s')", $this->getAttribute('name'))); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Filter.php b/vendor/twig/twig/lib/Twig/Node/Expression/Filter.php new file mode 100644 index 000000000..207b062a3 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Filter.php @@ -0,0 +1,36 @@ + $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()); + } + + $this->compileCallable($compiler); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Filter/Default.php b/vendor/twig/twig/lib/Twig/Node/Expression/Filter/Default.php new file mode 100644 index 000000000..1827c8886 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Filter/Default.php @@ -0,0 +1,43 @@ + + * {{ var.foo|default('foo item on var is not defined') }} + * + * + * @author Fabien Potencier + */ +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/vendor/twig/twig/lib/Twig/Node/Expression/Function.php b/vendor/twig/twig/lib/Twig/Node/Expression/Function.php new file mode 100644 index 000000000..3e1f6b559 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Function.php @@ -0,0 +1,35 @@ + $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()); + } + + $this->compileCallable($compiler); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/GetAttr.php b/vendor/twig/twig/lib/Twig/Node/Expression/GetAttr.php new file mode 100644 index 000000000..55d9fcc32 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/GetAttr.php @@ -0,0 +1,53 @@ + $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')); + + if (count($this->getNode('arguments')) || Twig_Template::ANY_CALL !== $this->getAttribute('type') || $this->getAttribute('is_defined_test') || $this->getAttribute('ignore_strict_check')) { + $compiler->raw(', ')->subcompile($this->getNode('arguments')); + + if (Twig_Template::ANY_CALL !== $this->getAttribute('type') || $this->getAttribute('is_defined_test') || $this->getAttribute('ignore_strict_check')) { + $compiler->raw(', ')->repr($this->getAttribute('type')); + } + + if ($this->getAttribute('is_defined_test') || $this->getAttribute('ignore_strict_check')) { + $compiler->raw(', '.($this->getAttribute('is_defined_test') ? 'true' : 'false')); + } + + if ($this->getAttribute('ignore_strict_check')) { + $compiler->raw(', '.($this->getAttribute('ignore_strict_check') ? 'true' : 'false')); + } + } + + $compiler->raw(')'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/MethodCall.php b/vendor/twig/twig/lib/Twig/Node/Expression/MethodCall.php new file mode 100644 index 000000000..620b02bf4 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/MethodCall.php @@ -0,0 +1,41 @@ + $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/vendor/twig/twig/lib/Twig/Node/Expression/Name.php b/vendor/twig/twig/lib/Twig/Node/Expression/Name.php new file mode 100644 index 000000000..3b8fae01d --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Name.php @@ -0,0 +1,88 @@ + '$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'); + + 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 (version_compare(phpversion(), '5.4.0RC1', '>=')) { + // 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/vendor/twig/twig/lib/Twig/Node/Expression/Parent.php b/vendor/twig/twig/lib/Twig/Node/Expression/Parent.php new file mode 100644 index 000000000..a22ce0386 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Parent.php @@ -0,0 +1,47 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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/vendor/twig/twig/lib/Twig/Node/Expression/TempName.php b/vendor/twig/twig/lib/Twig/Node/Expression/TempName.php new file mode 100644 index 000000000..e6b058e85 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/TempName.php @@ -0,0 +1,26 @@ + $name), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler + ->raw('$_') + ->raw($this->getAttribute('name')) + ->raw('_') + ; + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Test.php b/vendor/twig/twig/lib/Twig/Node/Expression/Test.php new file mode 100644 index 000000000..639f501a1 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Test.php @@ -0,0 +1,32 @@ + $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()); + } + + $this->compileCallable($compiler); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Test/Constant.php b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Constant.php new file mode 100644 index 000000000..de55f5f55 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Constant.php @@ -0,0 +1,46 @@ + + * {% if post.status is constant('Post::PUBLISHED') %} + * the status attribute is exactly the same as Post::PUBLISHED + * {% endif %} + * + * + * @author Fabien Potencier + */ +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/vendor/twig/twig/lib/Twig/Node/Expression/Test/Defined.php b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Defined.php new file mode 100644 index 000000000..247b2e23a --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Defined.php @@ -0,0 +1,54 @@ + + * {# defined works with variable names and variable attributes #} + * {% if foo is defined %} + * {# ... #} + * {% endif %} + * + * + * @author Fabien Potencier + */ +class Twig_Node_Expression_Test_Defined extends Twig_Node_Expression_Test +{ + public function __construct(Twig_NodeInterface $node, $name, Twig_NodeInterface $arguments = null, $lineno) + { + parent::__construct($node, $name, $arguments, $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); + } else { + throw new Twig_Error_Syntax('The "defined" test only works with simple variables', $this->getLine()); + } + } + + 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/vendor/twig/twig/lib/Twig/Node/Expression/Test/Divisibleby.php b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Divisibleby.php new file mode 100644 index 000000000..d5bed234a --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Divisibleby.php @@ -0,0 +1,33 @@ + + * {% if loop.index is divisible by(3) %} + * + * + * @author Fabien Potencier + */ +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/vendor/twig/twig/lib/Twig/Node/Expression/Test/Even.php b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Even.php new file mode 100644 index 000000000..d7853e89c --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Even.php @@ -0,0 +1,32 @@ + + * {{ var is even }} + * + * + * @author Fabien Potencier + */ +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/vendor/twig/twig/lib/Twig/Node/Expression/Test/Null.php b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Null.php new file mode 100644 index 000000000..1c83825a0 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Null.php @@ -0,0 +1,31 @@ + + * {{ var is none }} + * + * + * @author Fabien Potencier + */ +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/vendor/twig/twig/lib/Twig/Node/Expression/Test/Odd.php b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Odd.php new file mode 100644 index 000000000..421c19e84 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Odd.php @@ -0,0 +1,32 @@ + + * {{ var is odd }} + * + * + * @author Fabien Potencier + */ +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/vendor/twig/twig/lib/Twig/Node/Expression/Test/Sameas.php b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Sameas.php new file mode 100644 index 000000000..b48905ee4 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Test/Sameas.php @@ -0,0 +1,29 @@ + + */ +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/vendor/twig/twig/lib/Twig/Node/Expression/Unary.php b/vendor/twig/twig/lib/Twig/Node/Expression/Unary.php new file mode 100644 index 000000000..c514388eb --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Unary.php @@ -0,0 +1,30 @@ + $node), array(), $lineno); + } + + public function compile(Twig_Compiler $compiler) + { + $compiler->raw('('); + $this->operator($compiler); + $compiler + ->subcompile($this->getNode('node')) + ->raw(')') + ; + } + + abstract public function operator(Twig_Compiler $compiler); +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Neg.php b/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Neg.php new file mode 100644 index 000000000..2a3937ec3 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Neg.php @@ -0,0 +1,18 @@ +raw('-'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Not.php b/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Not.php new file mode 100644 index 000000000..f94073cfa --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Not.php @@ -0,0 +1,18 @@ +raw('!'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Pos.php b/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Pos.php new file mode 100644 index 000000000..04edb52a6 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Expression/Unary/Pos.php @@ -0,0 +1,18 @@ +raw('+'); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Flush.php b/vendor/twig/twig/lib/Twig/Node/Flush.php new file mode 100644 index 000000000..20d6aab43 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Flush.php @@ -0,0 +1,36 @@ + + */ +class Twig_Node_Flush extends Twig_Node +{ + public function __construct($lineno, $tag) + { + parent::__construct(array(), array(), $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write("flush();\n") + ; + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/For.php b/vendor/twig/twig/lib/Twig/Node/For.php new file mode 100644 index 000000000..c54a23cc9 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/For.php @@ -0,0 +1,112 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + // the (array) cast bypasses a PHP 5.2.6 bug + ->write("\$context['_parent'] = (array) \$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/vendor/twig/twig/lib/Twig/Node/ForLoop.php b/vendor/twig/twig/lib/Twig/Node/ForLoop.php new file mode 100644 index 000000000..d330283e7 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/ForLoop.php @@ -0,0 +1,55 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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/vendor/twig/twig/lib/Twig/Node/If.php b/vendor/twig/twig/lib/Twig/Node/If.php new file mode 100644 index 000000000..980274e5c --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/If.php @@ -0,0 +1,66 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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/vendor/twig/twig/lib/Twig/Node/Import.php b/vendor/twig/twig/lib/Twig/Node/Import.php new file mode 100644 index 000000000..230665541 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Import.php @@ -0,0 +1,50 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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->env->loadTemplate(') + ->subcompile($this->getNode('expr')) + ->raw(")") + ; + } + + $compiler->raw(";\n"); + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Include.php b/vendor/twig/twig/lib/Twig/Node/Include.php new file mode 100644 index 000000000..06548884a --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Include.php @@ -0,0 +1,85 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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) + { + $method = $this->getNode('expr') instanceof Twig_Node_Expression_Constant ? 'loadTemplate' : 'resolveTemplate'; + $compiler + ->write(sprintf('$this->env->%s(', $method)) + ->subcompile($this->getNode('expr')) + ->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/vendor/twig/twig/lib/Twig/Node/Macro.php b/vendor/twig/twig/lib/Twig/Node/Macro.php new file mode 100644 index 000000000..47c2915f7 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Macro.php @@ -0,0 +1,96 @@ + + */ +class Twig_Node_Macro extends Twig_Node +{ + public function __construct($name, Twig_NodeInterface $body, Twig_NodeInterface $arguments, $lineno, $tag = null) + { + parent::__construct(array('body' => $body, 'arguments' => $arguments), array('name' => $name), $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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(', '); + } + } + + $compiler + ->raw(")\n") + ->write("{\n") + ->indent() + ; + + if (!count($this->getNode('arguments'))) { + $compiler->write("\$context = \$this->env->getGlobals();\n\n"); + } else { + $compiler + ->write("\$context = \$this->env->mergeGlobals(array(\n") + ->indent() + ; + + foreach ($this->getNode('arguments') as $name => $default) { + $compiler + ->write('') + ->string($name) + ->raw(' => $_'.$name) + ->raw(",\n") + ; + } + + $compiler + ->outdent() + ->write("));\n\n") + ; + } + + $compiler + ->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("}\n\n") + ->write("return ('' === \$tmp = ob_get_clean()) ? '' : new Twig_Markup(\$tmp, \$this->env->getCharset());\n") + ->outdent() + ->write("}\n\n") + ; + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Module.php b/vendor/twig/twig/lib/Twig/Node/Module.php new file mode 100644 index 000000000..9f66b28d5 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Module.php @@ -0,0 +1,383 @@ + + */ +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), array('filename' => $filename, 'index' => null, 'embedded_templates' => $embeddedTemplates), 1); + } + + public function setIndex($index) + { + $this->setAttribute('index', $index); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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('compileClassHeader($compiler); + + if (count($this->getNode('blocks')) || count($this->getNode('traits')) || null === $this->getNode('parent') || $this->getNode('parent') instanceof Twig_Node_Expression_Constant) { + $this->compileConstructor($compiler); + } + + $this->compileGetParent($compiler); + + $this->compileDisplayHeader($compiler); + + $this->compileDisplayBody($compiler); + + $this->compileDisplayFooter($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 === $this->getNode('parent')) { + return; + } + + $compiler + ->write("protected function doGetParent(array \$context)\n", "{\n") + ->indent() + ->write("return ") + ; + + if ($this->getNode('parent') instanceof Twig_Node_Expression_Constant) { + $compiler->subcompile($this->getNode('parent')); + } else { + $compiler + ->raw("\$this->env->resolveTemplate(") + ->subcompile($this->getNode('parent')) + ->raw(")") + ; + } + + $compiler + ->raw(";\n") + ->outdent() + ->write("}\n\n") + ; + } + + protected function compileDisplayBody(Twig_Compiler $compiler) + { + $compiler->subcompile($this->getNode('body')); + + if (null !== $this->getNode('parent')) { + if ($this->getNode('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"); + } + } + + 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() + ->write("parent::__construct(\$env);\n\n") + ; + + // parent + if (null === $this->getNode('parent')) { + $compiler->write("\$this->parent = false;\n\n"); + } elseif ($this->getNode('parent') instanceof Twig_Node_Expression_Constant) { + $compiler + ->write("\$this->parent = \$this->env->loadTemplate(") + ->subcompile($this->getNode('parent')) + ->raw(");\n\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() + ->write("}\n\n"); + ; + } + + protected function compileDisplayHeader(Twig_Compiler $compiler) + { + $compiler + ->write("protected function doDisplay(array \$context, array \$blocks = array())\n", "{\n") + ->indent() + ; + } + + protected function compileDisplayFooter(Twig_Compiler $compiler) + { + $compiler + ->outdent() + ->write("}\n\n") + ; + } + + protected function compileClassFooter(Twig_Compiler $compiler) + { + $compiler + ->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->env->loadTemplate(", $var)) + ->subcompile($node) + ->raw(");\n") + ; + } else { + $compiler + ->write(sprintf("%s = ", $var)) + ->subcompile($node) + ->raw(";\n") + ->write(sprintf("if (!%s", $var)) + ->raw(" instanceof Twig_Template) {\n") + ->indent() + ->write(sprintf("%s = \$this->env->loadTemplate(%s);\n", $var, $var)) + ->outdent() + ->write("}\n") + ; + } + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Print.php b/vendor/twig/twig/lib/Twig/Node/Print.php new file mode 100644 index 000000000..42635361a --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Print.php @@ -0,0 +1,39 @@ + + */ +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); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('echo ') + ->subcompile($this->getNode('expr')) + ->raw(";\n") + ; + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Sandbox.php b/vendor/twig/twig/lib/Twig/Node/Sandbox.php new file mode 100644 index 000000000..8ca772bcd --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Sandbox.php @@ -0,0 +1,47 @@ + + */ +class Twig_Node_Sandbox extends Twig_Node +{ + public function __construct(Twig_NodeInterface $body, $lineno, $tag = null) + { + parent::__construct(array('body' => $body), array(), $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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/vendor/twig/twig/lib/Twig/Node/SandboxedModule.php b/vendor/twig/twig/lib/Twig/Node/SandboxedModule.php new file mode 100644 index 000000000..0de4503ec --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/SandboxedModule.php @@ -0,0 +1,60 @@ + + */ +class Twig_Node_SandboxedModule extends Twig_Node_Module +{ + protected $usedFilters; + protected $usedTags; + protected $usedFunctions; + + public function __construct(Twig_Node_Module $node, array $usedFilters, array $usedTags, array $usedFunctions) + { + parent::__construct($node->getNode('body'), $node->getNode('parent'), $node->getNode('blocks'), $node->getNode('macros'), $node->getNode('traits'), $node->getAttribute('embedded_templates'), $node->getAttribute('filename')); + + $this->setAttribute('index', $node->getAttribute('index')); + + $this->usedFilters = $usedFilters; + $this->usedTags = $usedTags; + $this->usedFunctions = $usedFunctions; + } + + protected function compileDisplayBody(Twig_Compiler $compiler) + { + $compiler->write("\$this->checkSecurity();\n"); + + parent::compileDisplayBody($compiler); + } + + protected function compileDisplayFooter(Twig_Compiler $compiler) + { + parent::compileDisplayFooter($compiler); + + $compiler + ->write("protected function checkSecurity()\n", "{\n") + ->indent() + ->write("\$this->env->getExtension('sandbox')->checkSecurity(\n") + ->indent() + ->write(!$this->usedTags ? "array(),\n" : "array('".implode('\', \'', $this->usedTags)."'),\n") + ->write(!$this->usedFilters ? "array(),\n" : "array('".implode('\', \'', $this->usedFilters)."'),\n") + ->write(!$this->usedFunctions ? "array()\n" : "array('".implode('\', \'', $this->usedFunctions)."')\n") + ->outdent() + ->write(");\n") + ->outdent() + ->write("}\n\n") + ; + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/SandboxedPrint.php b/vendor/twig/twig/lib/Twig/Node/SandboxedPrint.php new file mode 100644 index 000000000..91872cccc --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/SandboxedPrint.php @@ -0,0 +1,59 @@ + + */ +class Twig_Node_SandboxedPrint extends Twig_Node_Print +{ + public function __construct(Twig_Node_Expression $expr, $lineno, $tag = null) + { + parent::__construct($expr, $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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 + */ + protected function removeNodeFilter($node) + { + if ($node instanceof Twig_Node_Expression_Filter) { + return $this->removeNodeFilter($node->getNode('node')); + } + + return $node; + } +} diff --git a/vendor/twig/twig/lib/Twig/Node/Set.php b/vendor/twig/twig/lib/Twig/Node/Set.php new file mode 100644 index 000000000..407d14734 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Set.php @@ -0,0 +1,101 @@ + + */ +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); + } + } + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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/vendor/twig/twig/lib/Twig/Node/SetTemp.php b/vendor/twig/twig/lib/Twig/Node/SetTemp.php new file mode 100644 index 000000000..3bdd1cb74 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/SetTemp.php @@ -0,0 +1,35 @@ + $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/vendor/twig/twig/lib/Twig/Node/Spaceless.php b/vendor/twig/twig/lib/Twig/Node/Spaceless.php new file mode 100644 index 000000000..1478c59ac --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Spaceless.php @@ -0,0 +1,40 @@ + + */ +class Twig_Node_Spaceless extends Twig_Node +{ + public function __construct(Twig_NodeInterface $body, $lineno, $tag = 'spaceless') + { + parent::__construct(array('body' => $body), array(), $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + 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/vendor/twig/twig/lib/Twig/Node/Text.php b/vendor/twig/twig/lib/Twig/Node/Text.php new file mode 100644 index 000000000..6863604e5 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Node/Text.php @@ -0,0 +1,39 @@ + + */ +class Twig_Node_Text extends Twig_Node implements Twig_NodeOutputInterface +{ + public function __construct($data, $lineno) + { + parent::__construct(array(), array('data' => $data), $lineno); + } + + /** + * Compiles the node to PHP. + * + * @param Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('echo ') + ->string($this->getAttribute('data')) + ->raw(";\n") + ; + } +} diff --git a/vendor/twig/twig/lib/Twig/NodeInterface.php b/vendor/twig/twig/lib/Twig/NodeInterface.php new file mode 100644 index 000000000..8077349b3 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/NodeInterface.php @@ -0,0 +1,31 @@ + + * + * @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/vendor/twig/twig/lib/Twig/NodeOutputInterface.php b/vendor/twig/twig/lib/Twig/NodeOutputInterface.php new file mode 100644 index 000000000..22172c09d --- /dev/null +++ b/vendor/twig/twig/lib/Twig/NodeOutputInterface.php @@ -0,0 +1,19 @@ + + */ +interface Twig_NodeOutputInterface +{ +} diff --git a/vendor/twig/twig/lib/Twig/NodeTraverser.php b/vendor/twig/twig/lib/Twig/NodeTraverser.php new file mode 100644 index 000000000..8178a55c0 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/NodeTraverser.php @@ -0,0 +1,88 @@ + + */ +class Twig_NodeTraverser +{ + protected $env; + protected $visitors; + + /** + * 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; + $this->visitors = array(); + 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 + */ + 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/vendor/twig/twig/lib/Twig/NodeVisitor/Escaper.php b/vendor/twig/twig/lib/Twig/NodeVisitor/Escaper.php new file mode 100644 index 000000000..cc4b3d717 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/NodeVisitor/Escaper.php @@ -0,0 +1,167 @@ + + */ +class Twig_NodeVisitor_Escaper implements Twig_NodeVisitorInterface +{ + 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(); + } + + /** + * 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) + { + if ($node instanceof Twig_Node_Module) { + if ($env->hasExtension('escaper') && $defaultStrategy = $env->getExtension('escaper')->getDefaultStrategy($node->getAttribute('filename'))) { + $this->defaultStrategy = $defaultStrategy; + } + $this->safeVars = 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; + } + + /** + * 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 The modified node + */ + public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_Module) { + $this->defaultStrategy = false; + $this->safeVars = 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/vendor/twig/twig/lib/Twig/NodeVisitor/Optimizer.php b/vendor/twig/twig/lib/Twig/NodeVisitor/Optimizer.php new file mode 100644 index 000000000..06f69dbe3 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/NodeVisitor/Optimizer.php @@ -0,0 +1,246 @@ + + */ +class Twig_NodeVisitor_Optimizer implements Twig_NodeVisitorInterface +{ + 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 $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} + */ + public function enterNode(Twig_NodeInterface $node, Twig_Environment $env) + { + if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { + $this->enterOptimizeFor($node, $env); + } + + if (!version_compare(phpversion(), '5.4.0RC1', '>=') && 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} + */ + public function leaveNode(Twig_NodeInterface $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 + */ + 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 + */ + 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); + } 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')) { + $this->addLoopToCurrent(); + } + + // 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(); + } + + // 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); + } + } + + 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/vendor/twig/twig/lib/Twig/NodeVisitor/SafeAnalysis.php b/vendor/twig/twig/lib/Twig/NodeVisitor/SafeAnalysis.php new file mode 100644 index 000000000..a5d06de2c --- /dev/null +++ b/vendor/twig/twig/lib/Twig/NodeVisitor/SafeAnalysis.php @@ -0,0 +1,139 @@ +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, + ); + } + + public function enterNode(Twig_NodeInterface $node, Twig_Environment $env) + { + return $node; + } + + public function leaveNode(Twig_NodeInterface $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/vendor/twig/twig/lib/Twig/NodeVisitor/Sandbox.php b/vendor/twig/twig/lib/Twig/NodeVisitor/Sandbox.php new file mode 100644 index 000000000..fb27045ba --- /dev/null +++ b/vendor/twig/twig/lib/Twig/NodeVisitor/Sandbox.php @@ -0,0 +1,92 @@ + + */ +class Twig_NodeVisitor_Sandbox implements Twig_NodeVisitorInterface +{ + protected $inAModule = false; + protected $tags; + protected $filters; + protected $functions; + + /** + * 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) + { + 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()) { + $this->tags[] = $node->getNodeTag(); + } + + // look for filters + if ($node instanceof Twig_Node_Expression_Filter) { + $this->filters[] = $node->getNode('filter')->getAttribute('value'); + } + + // look for functions + if ($node instanceof Twig_Node_Expression_Function) { + $this->functions[] = $node->getAttribute('name'); + } + + // 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; + } + + /** + * 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 The modified node + */ + public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_Module) { + $this->inAModule = false; + + return new Twig_Node_SandboxedModule($node, array_unique($this->filters), array_unique($this->tags), array_unique($this->functions)); + } + + return $node; + } + + /** + * {@inheritdoc} + */ + public function getPriority() + { + return 0; + } +} diff --git a/vendor/twig/twig/lib/Twig/NodeVisitorInterface.php b/vendor/twig/twig/lib/Twig/NodeVisitorInterface.php new file mode 100644 index 000000000..608adedb0 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/NodeVisitorInterface.php @@ -0,0 +1,47 @@ + + */ +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/vendor/twig/twig/lib/Twig/Parser.php b/vendor/twig/twig/lib/Twig/Parser.php new file mode 100644 index 000000000..549ce2bdf --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Parser.php @@ -0,0 +1,390 @@ + + */ +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 + $vars = get_object_vars($this); + unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser']); + $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) { + if (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) { + $error = sprintf('Unexpected tag name "%s"', $token->getValue()); + if (is_array($test) && isset($test[0]) && $test[0] instanceof Twig_TokenParserInterface) { + $error .= sprintf(' (expecting closing tag for the "%s" tag defined near line %s)', $test[0]->getTag(), $lineno); + } + + throw new Twig_Error_Syntax($error, $token->getLine(), $this->getFilename()); + } + + $message = sprintf('Unknown tag name "%s"', $token->getValue()); + if ($alternatives = $this->env->computeAlternatives($token->getValue(), array_keys($this->env->getTags()))) { + $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); + } + + throw new Twig_Error_Syntax($message, $token->getLine(), $this->getFilename()); + } + + $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 (null === $this->reservedMacroNames) { + $this->reservedMacroNames = array(); + $r = new ReflectionClass($this->env->getBaseTemplateClass()); + foreach ($r->getMethods() as $method) { + $this->reservedMacroNames[] = $method->getName(); + } + } + + if (in_array($name, $this->reservedMacroNames)) { + 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 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/vendor/twig/twig/lib/Twig/ParserInterface.php b/vendor/twig/twig/lib/Twig/ParserInterface.php new file mode 100644 index 000000000..8e7cc0a85 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/ParserInterface.php @@ -0,0 +1,31 @@ + + * + * @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/vendor/twig/twig/lib/Twig/Sandbox/SecurityError.php b/vendor/twig/twig/lib/Twig/Sandbox/SecurityError.php new file mode 100644 index 000000000..015bfaea2 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Sandbox/SecurityError.php @@ -0,0 +1,19 @@ + + */ +class Twig_Sandbox_SecurityError extends Twig_Error +{ +} diff --git a/vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicy.php b/vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicy.php new file mode 100644 index 000000000..66ee23320 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicy.php @@ -0,0 +1,119 @@ + + */ +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_SecurityError(sprintf('Tag "%s" is not allowed.', $tag)); + } + } + + foreach ($filters as $filter) { + if (!in_array($filter, $this->allowedFilters)) { + throw new Twig_Sandbox_SecurityError(sprintf('Filter "%s" is not allowed.', $filter)); + } + } + + foreach ($functions as $function) { + if (!in_array($function, $this->allowedFunctions)) { + throw new Twig_Sandbox_SecurityError(sprintf('Function "%s" is not allowed.', $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/vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicyInterface.php b/vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicyInterface.php new file mode 100644 index 000000000..6ab48e3cc --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicyInterface.php @@ -0,0 +1,24 @@ + + */ +interface Twig_Sandbox_SecurityPolicyInterface +{ + public function checkSecurity($tags, $filters, $functions); + + public function checkMethodAllowed($obj, $method); + + public function checkPropertyAllowed($obj, $method); +} diff --git a/vendor/twig/twig/lib/Twig/SimpleFilter.php b/vendor/twig/twig/lib/Twig/SimpleFilter.php new file mode 100644 index 000000000..d35c56334 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/SimpleFilter.php @@ -0,0 +1,94 @@ + + */ +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_safe' => null, + 'is_safe_callback' => null, + 'pre_escape' => null, + 'preserves_safety' => null, + 'node_class' => 'Twig_Node_Expression_Filter', + ), $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']; + } +} diff --git a/vendor/twig/twig/lib/Twig/SimpleFunction.php b/vendor/twig/twig/lib/Twig/SimpleFunction.php new file mode 100644 index 000000000..8ef6aca2f --- /dev/null +++ b/vendor/twig/twig/lib/Twig/SimpleFunction.php @@ -0,0 +1,84 @@ + + */ +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_safe' => null, + 'is_safe_callback' => null, + 'node_class' => 'Twig_Node_Expression_Function', + ), $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(); + } +} diff --git a/vendor/twig/twig/lib/Twig/SimpleTest.php b/vendor/twig/twig/lib/Twig/SimpleTest.php new file mode 100644 index 000000000..225459c9f --- /dev/null +++ b/vendor/twig/twig/lib/Twig/SimpleTest.php @@ -0,0 +1,46 @@ + + */ +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( + 'node_class' => 'Twig_Node_Expression_Test', + ), $options); + } + + public function getName() + { + return $this->name; + } + + public function getCallable() + { + return $this->callable; + } + + public function getNodeClass() + { + return $this->options['node_class']; + } +} diff --git a/vendor/twig/twig/lib/Twig/Template.php b/vendor/twig/twig/lib/Twig/Template.php new file mode 100644 index 000000000..1e23e8b4f --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Template.php @@ -0,0 +1,485 @@ + + */ +abstract class Twig_Template implements Twig_TemplateInterface +{ + protected static $cache = array(); + + protected $parent; + protected $parents; + protected $env; + protected $blocks; + protected $traits; + + /** + * Constructor. + * + * @param Twig_Environment $env A Twig_Environment instance + */ + public function __construct(Twig_Environment $env) + { + $this->env = $env; + $this->blocks = array(); + $this->traits = array(); + } + + /** + * Returns the template name. + * + * @return string The template name + */ + abstract public function getTemplateName(); + + /** + * {@inheritdoc} + */ + public function getEnvironment() + { + return $this->env; + } + + /** + * Returns the parent template. + * + * This method is for internal use only and should never be called + * directly. + * + * @return Twig_TemplateInterface|false The parent template or false if there is no parent + */ + public function getParent(array $context) + { + if (null !== $this->parent) { + return $this->parent; + } + + $parent = $this->doGetParent($context); + if (false === $parent) { + return false; + } elseif ($parent instanceof Twig_Template) { + $name = $parent->getTemplateName(); + $this->parents[$name] = $parent; + $parent = $name; + } elseif (!isset($this->parents[$parent])) { + $this->parents[$parent] = $this->env->loadTemplate($parent); + } + + 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 + */ + 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 + */ + 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) { + try { + $template->$block($context, $blocks); + } catch (Twig_Error $e) { + 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 + */ + 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 + */ + 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 + */ + 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 + */ + public function getBlockNames() + { + return array_keys($this->blocks); + } + + /** + * Returns all blocks. + * + * This method is for internal use only and should never be called + * directly. + * + * @return array An array of blocks + * + * @see hasBlock + */ + public function getBlocks() + { + return $this->blocks; + } + + /** + * {@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; + } + + 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 The content of the context variable + * + * @throws Twig_Error_Runtime if the variable does not exist and Twig is running in strict mode + */ + 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 = Twig_Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false) + { + // array + if (Twig_Template::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 (Twig_Template::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)) { + $message = sprintf('Key "%s" for array with keys "%s" does not exist', $arrayItem, implode(', ', array_keys($object))); + } elseif (Twig_Template::ARRAY_CALL === $type) { + $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s")', $item, gettype($object), $object); + } 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; + } + + throw new Twig_Error_Runtime(sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName()); + } + + // object property + if (Twig_Template::METHOD_CALL !== $type) { + 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'])) { + 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('Method "%s" for object "%s" does not exist', $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; + } + + /** + * This method is only useful when testing Twig. Do not use it. + */ + public static function clearCache() + { + self::$cache = array(); + } +} diff --git a/vendor/twig/twig/lib/Twig/TemplateInterface.php b/vendor/twig/twig/lib/Twig/TemplateInterface.php new file mode 100644 index 000000000..d178832eb --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TemplateInterface.php @@ -0,0 +1,48 @@ + + * + * @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/vendor/twig/twig/lib/Twig/Test.php b/vendor/twig/twig/lib/Twig/Test.php new file mode 100644 index 000000000..3baff8856 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Test.php @@ -0,0 +1,34 @@ + + * @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/vendor/twig/twig/lib/Twig/Test/Function.php b/vendor/twig/twig/lib/Twig/Test/Function.php new file mode 100644 index 000000000..4be6b9b92 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Test/Function.php @@ -0,0 +1,35 @@ + + * @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/vendor/twig/twig/lib/Twig/Test/IntegrationTestCase.php b/vendor/twig/twig/lib/Twig/Test/IntegrationTestCase.php new file mode 100644 index 000000000..724f09417 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Test/IntegrationTestCase.php @@ -0,0 +1,154 @@ + + * @author Karma Dordrak + */ +abstract class Twig_Test_IntegrationTestCase extends PHPUnit_Framework_TestCase +{ + abstract protected function getExtensions(); + abstract protected function getFixturesDir(); + + /** + * @dataProvider getTests + */ + public function testIntegration($file, $message, $condition, $templates, $exception, $outputs) + { + $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs); + } + + public function getTests() + { + $fixturesDir = realpath($this->getFixturesDir()); + $tests = array(); + + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fixturesDir), RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if (!preg_match('/\.test$/', $file)) { + 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 = $this->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 = $this->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); + } + + return $tests; + } + + 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 $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); + } + + try { + $template = $twig->loadTemplate('index.twig'); + } catch (Exception $e) { + if (false !== $exception) { + $this->assertEquals(trim($exception), trim(sprintf('%s: %s', get_class($e), $e->getMessage()))); + + 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->assertEquals(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) { + echo 'Compiled template that failed:'; + + 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/vendor/twig/twig/lib/Twig/Test/Method.php b/vendor/twig/twig/lib/Twig/Test/Method.php new file mode 100644 index 000000000..17c6c041d --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Test/Method.php @@ -0,0 +1,37 @@ + + * @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/vendor/twig/twig/lib/Twig/Test/Node.php b/vendor/twig/twig/lib/Twig/Test/Node.php new file mode 100644 index 000000000..c832a57bf --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Test/Node.php @@ -0,0 +1,37 @@ + + * @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/vendor/twig/twig/lib/Twig/Test/NodeTestCase.php b/vendor/twig/twig/lib/Twig/Test/NodeTestCase.php new file mode 100644 index 000000000..b15c85ffd --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Test/NodeTestCase.php @@ -0,0 +1,58 @@ +assertNodeCompilation($source, $node, $environment); + } + + public function assertNodeCompilation($source, Twig_Node $node, Twig_Environment $environment = null) + { + $compiler = $this->getCompiler($environment); + $compiler->compile($node); + + $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(); + } + + protected function getVariableGetter($name) + { + if (version_compare(phpversion(), '5.4.0RC1', '>=')) { + return sprintf('(isset($context["%s"]) ? $context["%s"] : null)', $name, $name); + } + + return sprintf('$this->getContext($context, "%s")', $name); + } + + protected function getAttributeGetter() + { + if (function_exists('twig_template_get_attributes')) { + return 'twig_template_get_attributes($this, '; + } + + return '$this->getAttribute('; + } +} diff --git a/vendor/twig/twig/lib/Twig/TestCallableInterface.php b/vendor/twig/twig/lib/Twig/TestCallableInterface.php new file mode 100644 index 000000000..0db43682d --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TestCallableInterface.php @@ -0,0 +1,21 @@ + + * @deprecated since 1.12 (to be removed in 2.0) + */ +interface Twig_TestCallableInterface +{ + public function getCallable(); +} diff --git a/vendor/twig/twig/lib/Twig/TestInterface.php b/vendor/twig/twig/lib/Twig/TestInterface.php new file mode 100644 index 000000000..30d8a2c4f --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TestInterface.php @@ -0,0 +1,26 @@ + + * @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/vendor/twig/twig/lib/Twig/Token.php b/vendor/twig/twig/lib/Twig/Token.php new file mode 100644 index 000000000..599f9f565 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/Token.php @@ -0,0 +1,216 @@ + + */ +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/vendor/twig/twig/lib/Twig/TokenParser.php b/vendor/twig/twig/lib/Twig/TokenParser.php new file mode 100644 index 000000000..decebd5ee --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser.php @@ -0,0 +1,33 @@ + + */ +abstract class Twig_TokenParser implements Twig_TokenParserInterface +{ + /** + * @var Twig_Parser + */ + protected $parser; + + /** + * Sets the parser associated with this token parser + * + * @param $parser A Twig_Parser instance + */ + public function setParser(Twig_Parser $parser) + { + $this->parser = $parser; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/AutoEscape.php b/vendor/twig/twig/lib/Twig/TokenParser/AutoEscape.php new file mode 100644 index 000000000..275602883 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/AutoEscape.php @@ -0,0 +1,89 @@ + + * {% 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 %} + * + */ +class Twig_TokenParser_AutoEscape extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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 Boolean.', $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)) { + 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'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'autoescape'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Block.php b/vendor/twig/twig/lib/Twig/TokenParser/Block.php new file mode 100644 index 000000000..81e6b1cff --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Block.php @@ -0,0 +1,81 @@ + + * {% block head %} + * + * {% block title %}{% endblock %} - My Webpage + * {% endblock %} + * + */ +class Twig_TokenParser_Block extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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 '$name' has already been defined line %d", $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 '$name' (but %s given)", $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'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'block'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Do.php b/vendor/twig/twig/lib/Twig/TokenParser/Do.php new file mode 100644 index 000000000..f50939dd2 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Do.php @@ -0,0 +1,42 @@ +parser->getExpressionParser()->parseExpression(); + + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_Do($expr, $token->getLine(), $this->getTag()); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'do'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Embed.php b/vendor/twig/twig/lib/Twig/TokenParser/Embed.php new file mode 100644 index 000000000..69cb5f35f --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Embed.php @@ -0,0 +1,66 @@ +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'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'embed'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Extends.php b/vendor/twig/twig/lib/Twig/TokenParser/Extends.php new file mode 100644 index 000000000..f5ecee214 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Extends.php @@ -0,0 +1,52 @@ + + * {% extends "base.html" %} + * + */ +class Twig_TokenParser_Extends extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'extends'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Filter.php b/vendor/twig/twig/lib/Twig/TokenParser/Filter.php new file mode 100644 index 000000000..2b97475ae --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Filter.php @@ -0,0 +1,61 @@ + + * {% filter upper %} + * This text becomes uppercase + * {% endfilter %} + * + */ +class Twig_TokenParser_Filter extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'filter'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Flush.php b/vendor/twig/twig/lib/Twig/TokenParser/Flush.php new file mode 100644 index 000000000..4e15e7853 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Flush.php @@ -0,0 +1,42 @@ +parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_Flush($token->getLine(), $this->getTag()); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'flush'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/For.php b/vendor/twig/twig/lib/Twig/TokenParser/For.php new file mode 100644 index 000000000..5c07d6395 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/For.php @@ -0,0 +1,135 @@ + + *
    + * {% for user in users %} + *
  • {{ user.username|e }}
  • + * {% endfor %} + *
+ * + */ +class Twig_TokenParser_For extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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); + } + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'for'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/From.php b/vendor/twig/twig/lib/Twig/TokenParser/From.php new file mode 100644 index 000000000..dd73f9908 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/From.php @@ -0,0 +1,70 @@ + + * {% from 'forms.html' import forms %} + * + */ +class Twig_TokenParser_From extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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) { + $this->parser->addImportedSymbol('function', $alias, 'get'.$name, $node->getNode('var')); + } + + return $node; + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'from'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/If.php b/vendor/twig/twig/lib/Twig/TokenParser/If.php new file mode 100644 index 000000000..3d7d1f517 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/If.php @@ -0,0 +1,94 @@ + + * {% if users %} + *
    + * {% for user in users %} + *
  • {{ user.username|e }}
  • + * {% endfor %} + *
+ * {% endif %} + * + */ +class Twig_TokenParser_If extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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')); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'if'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Import.php b/vendor/twig/twig/lib/Twig/TokenParser/Import.php new file mode 100644 index 000000000..e7050c70c --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Import.php @@ -0,0 +1,49 @@ + + * {% import 'forms.html' as forms %} + * + */ +class Twig_TokenParser_Import extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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()); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'import'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Include.php b/vendor/twig/twig/lib/Twig/TokenParser/Include.php new file mode 100644 index 000000000..9c3099a68 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Include.php @@ -0,0 +1,75 @@ + + * {% include 'header.html' %} + * Body + * {% include 'footer.html' %} + * + */ +class Twig_TokenParser_Include extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'include'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Macro.php b/vendor/twig/twig/lib/Twig/TokenParser/Macro.php new file mode 100644 index 000000000..87a299d82 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Macro.php @@ -0,0 +1,68 @@ + + * {% macro input(name, value, type, size) %} + * + * {% endmacro %} + * + */ +class Twig_TokenParser_Macro extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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 '$name' (but %s given)", $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'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'macro'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Sandbox.php b/vendor/twig/twig/lib/Twig/TokenParser/Sandbox.php new file mode 100644 index 000000000..9457325ab --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Sandbox.php @@ -0,0 +1,68 @@ + + * {% sandbox %} + * {% include 'user.html' %} + * {% endsandbox %} + * + * + * @see http://www.twig-project.org/doc/api.html#sandbox-extension for details + */ +class Twig_TokenParser_Sandbox extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'sandbox'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Set.php b/vendor/twig/twig/lib/Twig/TokenParser/Set.php new file mode 100644 index 000000000..84f7e94cb --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Set.php @@ -0,0 +1,83 @@ + + * {% 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 %} + * + */ +class Twig_TokenParser_Set extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'set'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Spaceless.php b/vendor/twig/twig/lib/Twig/TokenParser/Spaceless.php new file mode 100644 index 000000000..1e3fa8f3e --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Spaceless.php @@ -0,0 +1,59 @@ + + * {% spaceless %} + *
+ * foo + *
+ * {% endspaceless %} + * + * {# output will be
foo
#} + * + */ +class Twig_TokenParser_Spaceless extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'spaceless'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParser/Use.php b/vendor/twig/twig/lib/Twig/TokenParser/Use.php new file mode 100644 index 000000000..3ea68b1a5 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParser/Use.php @@ -0,0 +1,76 @@ + + * {% extends "base.html" %} + * + * {% use "blocks.html" %} + * + * {% block title %}{% endblock %} + * {% block content %}{% endblock %} + * + * + * @see http://www.twig-project.org/doc/templates.html#horizontal-reuse for details. + */ +class Twig_TokenParser_Use extends Twig_TokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Twig_Token $token A Twig_Token instance + * + * @return Twig_NodeInterface A Twig_NodeInterface instance + */ + 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)))); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'use'; + } +} diff --git a/vendor/twig/twig/lib/Twig/TokenParserBroker.php b/vendor/twig/twig/lib/Twig/TokenParserBroker.php new file mode 100644 index 000000000..ec3fba674 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParserBroker.php @@ -0,0 +1,136 @@ + + * @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 + */ + public function __construct($parsers = array(), $brokers = array()) + { + 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/vendor/twig/twig/lib/Twig/TokenParserBrokerInterface.php b/vendor/twig/twig/lib/Twig/TokenParserBrokerInterface.php new file mode 100644 index 000000000..3f006e339 --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParserBrokerInterface.php @@ -0,0 +1,45 @@ + + * @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/vendor/twig/twig/lib/Twig/TokenParserInterface.php b/vendor/twig/twig/lib/Twig/TokenParserInterface.php new file mode 100644 index 000000000..31e8d5d5e --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenParserInterface.php @@ -0,0 +1,43 @@ + + */ +interface Twig_TokenParserInterface +{ + /** + * Sets the parser associated with this token parser + * + * @param $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/vendor/twig/twig/lib/Twig/TokenStream.php b/vendor/twig/twig/lib/Twig/TokenStream.php new file mode 100644 index 000000000..44440daed --- /dev/null +++ b/vendor/twig/twig/lib/Twig/TokenStream.php @@ -0,0 +1,156 @@ + + */ +class Twig_TokenStream +{ + protected $tokens; + protected $current; + 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->current = 0; + $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; + } +}