Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,71 @@
<?php
/**
* @file
* Contains \Drupal\Component\Annotation\AnnotationBase.
*/
namespace Drupal\Component\Annotation;
/**
* Provides a base class for classed annotations.
*/
abstract class AnnotationBase implements AnnotationInterface {
/**
* The annotated class ID.
*
* @var string
*/
public $id;
/**
* The class used for this annotated class.
*
* @var string
*/
protected $class;
/**
* The provider of the annotated class.
*
* @var string
*/
protected $provider;
/**
* {@inheritdoc}
*/
public function getProvider() {
return $this->provider;
}
/**
* {@inheritdoc}
*/
public function setProvider($provider) {
$this->provider = $provider;
}
/**
* {@inheritdoc}
*/
public function getId() {
return $this->id;
}
/**
* {@inheritdoc}
*/
public function getClass() {
return $this->class;
}
/**
* {@inheritdoc}
*/
public function setClass($class) {
$this->class = $class;
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\Component\Annotation\AnnotationInterface.
*/
namespace Drupal\Component\Annotation;
/**
* Defines a common interface for classed annotations.
*/
interface AnnotationInterface {
/**
* Gets the value of an annotation.
*/
public function get();
/**
* Gets the name of the provider of the annotated class.
*
* @return string
*/
public function getProvider();
/**
* Sets the name of the provider of the annotated class.
*
* @param string $provider
*/
public function setProvider($provider);
/**
* Gets the unique ID for this annotated class.
*
* @return string
*/
public function getId();
/**
* Gets the class of the annotated class.
*
* @return string
*/
public function getClass();
/**
* Sets the class of the annotated class.
*
* @param string $class
*/
public function setClass($class);
}

View file

@ -0,0 +1,117 @@
<?php
/**
* @file
* Contains \Drupal\Component\Annotation\Plugin.
*/
namespace Drupal\Component\Annotation;
use Drupal\Component\Utility\NestedArray;
/**
* Defines a Plugin annotation object.
*
* Annotations in plugin classes can use this class in order to pass various
* metadata about the plugin through the parser to
* DiscoveryInterface::getDefinitions() calls. This allows the metadata
* of a class to be located with the class itself, rather than in module-based
* info hooks.
*
* @ingroup plugin_api
*
* @Annotation
*/
class Plugin implements AnnotationInterface {
/**
* The plugin definition read from the class annotation.
*
* @var array
*/
protected $definition;
/**
* Constructs a Plugin object.
*
* Builds up the plugin definition and invokes the get() method for any
* classed annotations that were used.
*/
public function __construct($values) {
$reflection = new \ReflectionClass($this);
// Only keep actual default values by ignoring NULL values.
$defaults = array_filter($reflection->getDefaultProperties(), function ($value) {
return $value !== NULL;
});
$parsed_values = $this->parse($values);
$this->definition = NestedArray::mergeDeep($defaults, $parsed_values);
}
/**
* Parses an annotation into its definition.
*
* @param array $values
* The annotation array.
*
* @return array
* The parsed annotation as a definition.
*/
protected function parse(array $values) {
$definitions = array();
foreach ($values as $key => $value) {
if ($value instanceof AnnotationInterface) {
$definitions[$key] = $value->get();
}
elseif (is_array($value)) {
$definitions[$key] = $this->parse($value);
}
else {
$definitions[$key] = $value;
}
}
return $definitions;
}
/**
* {@inheritdoc}
*/
public function get() {
return $this->definition;
}
/**
* {@inheritdoc}
*/
public function getProvider() {
return isset($this->definition['provider']) ? $this->definition['provider'] : FALSE;
}
/**
* {@inheritdoc}
*/
public function setProvider($provider) {
$this->definition['provider'] = $provider;
}
/**
* {@inheritdoc}
*/
public function getId() {
return $this->definition['id'];
}
/**
* {@inheritdoc}
*/
public function getClass() {
return $this->definition['class'];
}
/**
* {@inheritdoc}
*/
public function setClass($class) {
$this->definition['class'] = $class;
}
}

View file

@ -0,0 +1,151 @@
<?php
/**
* @file
* Contains \Drupal\Component\Annotation\Plugin\Discovery\AnnotatedClassDiscovery.
*/
namespace Drupal\Component\Annotation\Plugin\Discovery;
use Drupal\Component\Annotation\AnnotationInterface;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Annotation\Reflection\MockFileFinder;
use Doctrine\Common\Annotations\SimpleAnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Reflection\StaticReflectionParser;
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
/**
* Defines a discovery mechanism to find annotated plugins in PSR-0 namespaces.
*/
class AnnotatedClassDiscovery implements DiscoveryInterface {
use DiscoveryTrait;
/**
* The namespaces within which to find plugin classes.
*
* @var string[]
*/
protected $pluginNamespaces;
/**
* The name of the annotation that contains the plugin definition.
*
* The class corresponding to this name must implement
* \Drupal\Component\Annotation\AnnotationInterface.
*
* @var string
*/
protected $pluginDefinitionAnnotationName;
/**
* The doctrine annotation reader.
*
* @var \Doctrine\Common\Annotations\Reader
*/
protected $annotationReader;
/**
* Constructs a new instance.
*
* @param string[] $plugin_namespaces
* (optional) An array of namespace that may contain plugin implementations.
* Defaults to an empty array.
* @param string $plugin_definition_annotation_name
* (optional) The name of the annotation that contains the plugin definition.
* Defaults to 'Drupal\Component\Annotation\Plugin'.
*/
function __construct($plugin_namespaces = array(), $plugin_definition_annotation_name = 'Drupal\Component\Annotation\Plugin') {
$this->pluginNamespaces = $plugin_namespaces;
$this->pluginDefinitionAnnotationName = $plugin_definition_annotation_name;
}
/**
* Gets the used doctrine annotation reader.
*
* @return \Doctrine\Common\Annotations\Reader
* The annotation reader.
*/
protected function getAnnotationReader() {
if (!isset($this->annotationReader)) {
$this->annotationReader = new SimpleAnnotationReader();
// Add the namespaces from the main plugin annotation, like @EntityType.
$namespace = substr($this->pluginDefinitionAnnotationName, 0, strrpos($this->pluginDefinitionAnnotationName, '\\'));
$this->annotationReader->addNamespace($namespace);
}
return $this->annotationReader;
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
$definitions = array();
$reader = $this->getAnnotationReader();
// Clear the annotation loaders of any previous annotation classes.
AnnotationRegistry::reset();
// Register the namespaces of classes that can be used for annotations.
AnnotationRegistry::registerLoader('class_exists');
// Search for classes within all PSR-0 namespace locations.
foreach ($this->getPluginNamespaces() as $namespace => $dirs) {
foreach ($dirs as $dir) {
if (file_exists($dir)) {
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $fileinfo) {
if ($fileinfo->getExtension() == 'php') {
$sub_path = $iterator->getSubIterator()->getSubPath();
$sub_path = $sub_path ? str_replace(DIRECTORY_SEPARATOR, '\\', $sub_path) . '\\' : '';
$class = $namespace . '\\' . $sub_path . $fileinfo->getBasename('.php');
// The filename is already known, so there is no need to find the
// file. However, StaticReflectionParser needs a finder, so use a
// mock version.
$finder = MockFileFinder::create($fileinfo->getPathName());
$parser = new StaticReflectionParser($class, $finder, TRUE);
/** @var $annotation \Drupal\Component\Annotation\AnnotationInterface */
if ($annotation = $reader->getClassAnnotation($parser->getReflectionClass(), $this->pluginDefinitionAnnotationName)) {
$this->prepareAnnotationDefinition($annotation, $class);
$definitions[$annotation->getId()] = $annotation->get();
}
}
}
}
}
}
// Don't let annotation loaders pile up.
AnnotationRegistry::reset();
return $definitions;
}
/**
* Prepares the annotation definition.
*
* @param \Drupal\Component\Annotation\AnnotationInterface $annotation
* The annotation derived from the plugin.
* @param string $class
* The class used for the plugin.
*/
protected function prepareAnnotationDefinition(AnnotationInterface $annotation, $class) {
$annotation->setClass($class);
}
/**
* Gets an array of PSR-0 namespaces to search for plugin classes.
*
* @return string[]
*/
protected function getPluginNamespaces() {
return $this->pluginNamespaces;
}
}

View file

@ -0,0 +1,44 @@
<?php
/**
* @file
* Contains \Drupal\Component\Annotation\PluginID.
*/
namespace Drupal\Component\Annotation;
/**
* Defines a Plugin annotation object that just contains an ID.
*
* @Annotation
*/
class PluginID extends AnnotationBase {
/**
* The plugin ID.
*
* When an annotation is given no key, 'value' is assumed by Doctrine.
*
* @var string
*/
public $value;
/**
* {@inheritdoc}
*/
public function get() {
return array(
'id' => $this->value,
'class' => $this->class,
'provider' => $this->provider,
);
}
/**
* {@inheritdoc}
*/
public function getId() {
return $this->value;
}
}

View file

@ -0,0 +1,44 @@
<?php
/**
* @file
* Contains \Drupal\Component\Annotation\Reflection\MockFileFinder.
*/
namespace Drupal\Component\Annotation\Reflection;
use Doctrine\Common\Reflection\ClassFinderInterface;
/**
* Defines a mock file finder that only returns a single filename.
*
* This can be used with Doctrine\Common\Reflection\StaticReflectionParser if
* the filename is known and inheritance is not a concern (for example, if
* only the class annotation is needed).
*/
class MockFileFinder implements ClassFinderInterface {
/**
* The only filename this finder ever returns.
*
* @var string
*/
protected $filename;
/**
* Implements Doctrine\Common\Reflection\ClassFinderInterface::findFile().
*/
public function findFile($class) {
return $this->filename;
}
/**
* Creates new mock file finder objects.
*/
static public function create($filename) {
$object = new static();
$object->filename = $filename;
return $object;
}
}

View file

@ -0,0 +1,109 @@
<?php
/**
* @file
* Contains \Drupal\Component\Bridge\ZfExtensionManagerSfContainer.
*/
namespace Drupal\Component\Bridge;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Zend\Feed\Reader\ExtensionManagerInterface as ReaderManagerInterface;
use Zend\Feed\Writer\ExtensionManagerInterface as WriterManagerInterface;
/**
* Defines a bridge between the ZF2 service manager to Symfony container.
*/
class ZfExtensionManagerSfContainer implements ReaderManagerInterface, WriterManagerInterface, ContainerAwareInterface {
/**
* This property was based from Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*
* A map of characters to be replaced through strtr.
*
* @var array
*
* @see \Drupal\Component\Bridge\ZfExtensionManagerSfContainer::canonicalizeName().
*/
protected $canonicalNamesReplacements = array('-' => '', '_' => '', ' ' => '', '\\' => '', '/' => '');
/**
* The prefix to be used when retrieving plugins from the container.
*
* @var string
*/
protected $prefix = '';
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* A local cache of computed canonical names.
*
* @var string[]
*/
protected $canonicalNames;
/**
* Constructs a ZfExtensionManagerSfContainer object.
*
* @param string $prefix
* The prefix to be used when retrieving plugins from the container.
*/
public function __construct($prefix = '') {
return $this->prefix = $prefix;
}
/**
* {@inheritdoc}
*/
public function get($extension) {
return $this->container->get($this->prefix . $this->canonicalizeName($extension));
}
/**
* {@inheritdoc}
*/
public function has($extension) {
return $this->container->has($this->prefix . $this->canonicalizeName($extension));
}
/**
* This method was based from Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*
* Canonicalize the extension name to a service name.
*
* @param string $name
* The extension name.
*
* @return string
* The service name, without the prefix.
*/
protected function canonicalizeName($name) {
if (isset($this->canonicalNames[$name])) {
return $this->canonicalNames[$name];
}
// This is just for performance instead of using str_replace().
return $this->canonicalNames[$name] = strtolower(strtr($name, $this->canonicalNamesReplacements));
}
/**
* {@inheritdoc}
*/
public function setContainer(ContainerInterface $container = NULL) {
$this->container = $container;
}
}

View file

@ -0,0 +1,600 @@
<?php
/**
* @file
* Contains \Drupal\Component\Datetime\DateTimePlus.
*/
namespace Drupal\Component\Datetime;
/**
* Wraps DateTime().
*
* This class wraps the PHP DateTime class with more flexible initialization
* parameters, allowing a date to be created from an existing date object,
* a timestamp, a string with an unknown format, a string with a known
* format, or an array of date parts. It also adds an errors array
* and a __toString() method to the date object.
*
* This class is less lenient than the DateTime class. It changes
* the default behavior for handling date values like '2011-00-00'.
* The DateTime class would convert that value to '2010-11-30' and report
* a warning but not an error. This extension treats that as an error.
*
* As with the DateTime class, a date object may be created even if it has
* errors. It has an errors array attached to it that explains what the
* errors are. This is less disruptive than allowing datetime exceptions
* to abort processing. The calling script can decide what to do about
* errors using hasErrors() and getErrors().
*/
class DateTimePlus {
const FORMAT = 'Y-m-d H:i:s';
/**
* A RFC7231 Compliant date.
*
* http://tools.ietf.org/html/rfc7231#section-7.1.1.1
*
* Example: Sun, 06 Nov 1994 08:49:37 GMT
*/
const RFC7231 = 'D, d M Y H:i:s \G\M\T';
/**
* An array of possible date parts.
*/
protected static $dateParts = array(
'year',
'month',
'day',
'hour',
'minute',
'second',
);
/**
* The value of the time value passed to the constructor.
*/
protected $inputTimeRaw = '';
/**
* The prepared time, without timezone, for this date.
*/
protected $inputTimeAdjusted = '';
/**
* The value of the timezone passed to the constructor.
*/
protected $inputTimeZoneRaw = '';
/**
* The prepared timezone object used to construct this date.
*/
protected $inputTimeZoneAdjusted = '';
/**
* The value of the format passed to the constructor.
*/
protected $inputFormatRaw = '';
/**
* The prepared format, if provided.
*/
protected $inputFormatAdjusted = '';
/**
* The value of the language code passed to the constructor.
*/
protected $langcode = NULL;
/**
* An array of errors encountered when creating this date.
*/
protected $errors = array();
/**
* The DateTime object.
*
* @var \DateTime
*/
protected $dateTimeObject = NULL;
/**
* Creates a date object from an input date object.
*
* @param \DateTime $datetime
* A DateTime object.
* @param array $settings
* @see __construct()
*/
public static function createFromDateTime(\DateTime $datetime, $settings = array()) {
return new static($datetime->format(static::FORMAT), $datetime->getTimezone(), $settings);
}
/**
* Creates a date object from an array of date parts.
*
* Converts the input value into an ISO date, forcing a full ISO
* date even if some values are missing.
*
* @param array $date_parts
* An array of date parts, like ('year' => 2014, 'month => 4).
* @param mixed $timezone
* (optional) \DateTimeZone object, time zone string or NULL. NULL uses the
* default system time zone. Defaults to NULL.
* @param array $settings
* (optional) A keyed array for settings, suitable for passing on to
* __construct().
*
* @return static
* A new \Drupal\Component\DateTimePlus object based on the parameters
* passed in.
*/
public static function createFromArray(array $date_parts, $timezone = NULL, $settings = array()) {
$date_parts = static::prepareArray($date_parts, TRUE);
if (static::checkArray($date_parts)) {
// Even with validation, we can end up with a value that the
// DateTime class won't handle, like a year outside the range
// of -9999 to 9999, which will pass checkdate() but
// fail to construct a date object.
$iso_date = static::arrayToISO($date_parts);
return new static($iso_date, $timezone, $settings);
}
else {
throw new \Exception('The array contains invalid values.');
}
}
/**
* Creates a date object from timestamp input.
*
* The timezone of a timestamp is always UTC. The timezone for a
* timestamp indicates the timezone used by the format() method.
*
* @param int $timestamp
* A UNIX timestamp.
* @param mixed $timezone
* @see __construct()
* @param array $settings
* @see __construct()
*/
public static function createFromTimestamp($timestamp, $timezone = NULL, $settings = array()) {
if (!is_numeric($timestamp)) {
throw new \Exception('The timestamp must be numeric.');
}
$datetime = new static('', $timezone, $settings);
$datetime->setTimestamp($timestamp);
return $datetime;
}
/**
* Creates a date object from an input format.
*
* @param string $format
* PHP date() type format for parsing the input. This is recommended
* to use things like negative years, which php's parser fails on, or
* any other specialized input with a known format. If provided the
* date will be created using the createFromFormat() method.
* @see http://us3.php.net/manual/en/datetime.createfromformat.php
* @param mixed $time
* @see __construct()
* @param mixed $timezone
* @see __construct()
* @param array $settings
* - validate_format: (optional) Boolean choice to validate the
* created date using the input format. The format used in
* createFromFormat() allows slightly different values than format().
* Using an input format that works in both functions makes it
* possible to a validation step to confirm that the date created
* from a format string exactly matches the input. This option
* indicates the format can be used for validation. Defaults to TRUE.
* @see __construct()
*/
public static function createFromFormat($format, $time, $timezone = NULL, $settings = array()) {
if (!isset($settings['validate_format'])) {
$settings['validate_format'] = TRUE;
}
// Tries to create a date from the format and use it if possible.
// A regular try/catch won't work right here, if the value is
// invalid it doesn't return an exception.
$datetimeplus = new static('', $timezone, $settings);
$date = \DateTime::createFromFormat($format, $time, $datetimeplus->getTimezone());
if (!$date instanceOf \DateTime) {
throw new \Exception('The date cannot be created from a format.');
}
else {
// Functions that parse date is forgiving, it might create a date that
// is not exactly a match for the provided value, so test for that by
// re-creating the date/time formatted string and comparing it to the input. For
// instance, an input value of '11' using a format of Y (4 digits) gets
// created as '0011' instead of '2011'.
if ($date instanceOf DateTimePlus) {
$test_time = $date->format($format, $settings);
}
elseif ($date instanceOf \DateTime) {
$test_time = $date->format($format);
}
$datetimeplus->setTimestamp($date->getTimestamp());
$datetimeplus->setTimezone($date->getTimezone());
if ($settings['validate_format'] && $test_time != $time) {
throw new \Exception('The created date does not match the input value.');
}
}
return $datetimeplus;
}
/**
* Constructs a date object set to a requested date and timezone.
*
* @param string $time
* (optional) A date/time string. Defaults to 'now'.
* @param mixed $timezone
* (optional) \DateTimeZone object, time zone string or NULL. NULL uses the
* default system time zone. Defaults to NULL.
* @param array $settings
* (optional) Keyed array of settings. Defaults to empty array.
* - langcode: (optional) String two letter language code used to control
* the result of the format(). Defaults to NULL.
* - debug: (optional) Boolean choice to leave debug values in the
* date object for debugging purposes. Defaults to FALSE.
*/
public function __construct($time = 'now', $timezone = NULL, $settings = array()) {
// Unpack settings.
$this->langcode = !empty($settings['langcode']) ? $settings['langcode'] : NULL;
// Massage the input values as necessary.
$prepared_time = $this->prepareTime($time);
$prepared_timezone = $this->prepareTimezone($timezone);
try {
if (!empty($prepared_time)) {
$test = date_parse($prepared_time);
if (!empty($test['errors'])) {
$this->errors[] = $test['errors'];
}
}
if (empty($this->errors)) {
$this->dateTimeObject = new \DateTime($prepared_time, $prepared_timezone);
}
}
catch (\Exception $e) {
$this->errors[] = $e->getMessage();
}
// Clean up the error messages.
$this->checkErrors();
$this->errors = array_unique($this->errors);
}
/**
* Implements __toString() for dates.
*
* The DateTime class does not implement this.
*
* @see https://bugs.php.net/bug.php?id=62911
* @see http://www.serverphorums.com/read.php?7,555645
*/
public function __toString() {
$format = static::FORMAT;
return $this->format($format) . ' ' . $this->getTimeZone()->getName();
}
/**
* Implements the magic __call method.
*
* Passes through all unknown calls onto the DateTime object.
*/
public function __call($method, $args) {
// @todo consider using assert() as per https://www.drupal.org/node/2451793.
if (!isset($this->dateTimeObject)) {
throw new \Exception('DateTime object not set.');
}
if (!method_exists($this->dateTimeObject, $method)) {
throw new \BadMethodCallException(sprintf('Call to undefined method %s::%s()', get_class($this), $method));
}
return call_user_func_array(array($this->dateTimeObject, $method), $args);
}
/**
* Implements the magic __callStatic method.
*
* Passes through all unknown static calls onto the DateTime object.
*/
public static function __callStatic($method, $args) {
if (!method_exists('\DateTime', $method)) {
throw new \BadMethodCallException(sprintf('Call to undefined method %s::%s()', get_called_class(), $method));
}
return call_user_func_array(array('\DateTime', $method), $args);
}
/**
* Implements the magic __clone method.
*
* Deep-clones the DateTime object we're wrapping.
*/
public function __clone() {
$this->dateTimeObject = clone($this->dateTimeObject);
}
/**
* Prepares the input time value.
*
* Changes the input value before trying to use it, if necessary.
* Can be overridden to handle special cases.
*
* @param mixed $time
* An input value, which could be a timestamp, a string,
* or an array of date parts.
*/
protected function prepareTime($time) {
return $time;
}
/**
* Prepares the input timezone value.
*
* Changes the timezone before trying to use it, if necessary.
* Most importantly, makes sure there is a valid timezone
* object before moving further.
*
* @param mixed $timezone
* Either a timezone name or a timezone object or NULL.
*/
protected function prepareTimezone($timezone) {
// If the input timezone is a valid timezone object, use it.
if ($timezone instanceOf \DateTimezone) {
$timezone_adjusted = $timezone;
}
// Allow string timezone input, and create a timezone from it.
elseif (!empty($timezone) && is_string($timezone)) {
$timezone_adjusted = new \DateTimeZone($timezone);
}
// Default to the system timezone when not explicitly provided.
// If the system timezone is missing, use 'UTC'.
if (empty($timezone_adjusted) || !$timezone_adjusted instanceOf \DateTimezone) {
$system_timezone = date_default_timezone_get();
$timezone_name = !empty($system_timezone) ? $system_timezone : 'UTC';
$timezone_adjusted = new \DateTimeZone($timezone_name);
}
// We are finally certain that we have a usable timezone.
return $timezone_adjusted;
}
/**
* Prepares the input format value.
*
* Changes the input format before trying to use it, if necessary.
* Can be overridden to handle special cases.
*
* @param string $format
* A PHP format string.
*/
protected function prepareFormat($format) {
return $format;
}
/**
* Examines getLastErrors() to see what errors to report.
*
* Two kinds of errors are important: anything that DateTime
* considers an error, and also a warning that the date was invalid.
* PHP creates a valid date from invalid data with only a warning,
* 2011-02-30 becomes 2011-03-03, for instance, but we don't want that.
*
* @see http://us3.php.net/manual/en/time.getlasterrors.php
*/
public function checkErrors() {
$errors = \DateTime::getLastErrors();
if (!empty($errors['errors'])) {
$this->errors += $errors['errors'];
}
// Most warnings are messages that the date could not be parsed
// which causes it to be altered. For validation purposes, a warning
// as bad as an error, because it means the constructed date does
// not match the input value.
if (!empty($errors['warnings'])) {
$this->errors[] = 'The date is invalid.';
}
}
/**
* Detects if there were errors in the processing of this date.
*/
public function hasErrors() {
return (boolean) count($this->errors);
}
/**
* Gets error messages.
*
* Public function to return the error messages.
*/
public function getErrors() {
return $this->errors;
}
/**
* Creates an ISO date from an array of values.
*
* @param array $array
* An array of date values keyed by date part.
* @param bool $force_valid_date
* (optional) Whether to force a full date by filling in missing
* values. Defaults to FALSE.
*
* @return string
* The date as an ISO string.
*/
public static function arrayToISO($array, $force_valid_date = FALSE) {
$array = static::prepareArray($array, $force_valid_date);
$input_time = '';
if ($array['year'] !== '') {
$input_time = static::datePad(intval($array['year']), 4);
if ($force_valid_date || $array['month'] !== '') {
$input_time .= '-' . static::datePad(intval($array['month']));
if ($force_valid_date || $array['day'] !== '') {
$input_time .= '-' . static::datePad(intval($array['day']));
}
}
}
if ($array['hour'] !== '') {
$input_time .= $input_time ? 'T' : '';
$input_time .= static::datePad(intval($array['hour']));
if ($force_valid_date || $array['minute'] !== '') {
$input_time .= ':' . static::datePad(intval($array['minute']));
if ($force_valid_date || $array['second'] !== '') {
$input_time .= ':' . static::datePad(intval($array['second']));
}
}
}
return $input_time;
}
/**
* Creates a complete array from a possibly incomplete array of date parts.
*
* @param array $array
* An array of date values keyed by date part.
* @param bool $force_valid_date
* (optional) Whether to force a valid date by filling in missing
* values with valid values or just to use empty values instead.
* Defaults to FALSE.
*
* @return array
* A complete array of date parts.
*/
public static function prepareArray($array, $force_valid_date = FALSE) {
if ($force_valid_date) {
$now = new \DateTime();
$array += array(
'year' => $now->format('Y'),
'month' => 1,
'day' => 1,
'hour' => 0,
'minute' => 0,
'second' => 0,
);
}
else {
$array += array(
'year' => '',
'month' => '',
'day' => '',
'hour' => '',
'minute' => '',
'second' => '',
);
}
return $array;
}
/**
* Checks that arrays of date parts will create a valid date.
*
* Checks that an array of date parts has a year, month, and day,
* and that those values create a valid date. If time is provided,
* verifies that the time values are valid. Sort of an
* equivalent to checkdate().
*
* @param array $array
* An array of datetime values keyed by date part.
*
* @return boolean
* TRUE if the datetime parts contain valid values, otherwise FALSE.
*/
public static function checkArray($array) {
$valid_date = FALSE;
$valid_time = TRUE;
// Check for a valid date using checkdate(). Only values that
// meet that test are valid.
if (array_key_exists('year', $array) && array_key_exists('month', $array) && array_key_exists('day', $array)) {
if (@checkdate($array['month'], $array['day'], $array['year'])) {
$valid_date = TRUE;
}
}
// Testing for valid time is reversed. Missing time is OK,
// but incorrect values are not.
foreach (array('hour', 'minute', 'second') as $key) {
if (array_key_exists($key, $array)) {
$value = $array[$key];
switch ($key) {
case 'hour':
if (!preg_match('/^([1-2][0-3]|[01]?[0-9])$/', $value)) {
$valid_time = FALSE;
}
break;
case 'minute':
case 'second':
default:
if (!preg_match('/^([0-5][0-9]|[0-9])$/', $value)) {
$valid_time = FALSE;
}
break;
}
}
}
return $valid_date && $valid_time;
}
/**
* Pads date parts with zeros.
*
* Helper function for a task that is often required when working with dates.
*
* @param int $value
* The value to pad.
* @param int $size
* (optional) Size expected, usually 2 or 4. Defaults to 2.
*
* @return string
* The padded value.
*/
public static function datePad($value, $size = 2) {
return sprintf("%0" . $size . "d", $value);
}
/**
* Formats the date for display.
*
* @param string $format
* A format string using either PHP's date().
* @param array $settings
* - timezone: (optional) String timezone name. Defaults to the timezone
* of the date object.
*
* @return string
* The formatted value of the date.
*/
public function format($format, $settings = array()) {
// If there were construction errors, we can't format the date.
if ($this->hasErrors()) {
return;
}
// Format the date and catch errors.
try {
// Clone the date/time object so we can change the time zone without
// disturbing the value stored in the object.
$dateTimeObject = clone $this->dateTimeObject;
if (isset($settings['timezone'])) {
$dateTimeObject->setTimezone(new \DateTimeZone($settings['timezone']));
}
$value = $dateTimeObject->format($format);
}
catch (\Exception $e) {
$this->errors[] = $e->getMessage();
}
return $value;
}
}

View file

@ -0,0 +1,177 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\Diff.
*/
namespace Drupal\Component\Diff;
use Drupal\Component\Diff\Engine\DiffEngine;
/**
* Class representing a 'diff' between two sequences of strings.
* @todo document
* @subpackage DifferenceEngine
*
* Copied from https://www.drupal.org/project/diff which was based PHP diff
* engine for phpwiki. (Taken from phpwiki-1.3.3) The original code in phpwiki
* was copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org> and
* licensed under GPL.
*/
class Diff {
/**
* The list of differences as an array of diff operations.
*
* @var \Drupal\Component\Diff\Engine\DiffOp[]
*/
protected $edits;
/**
* Constructor.
* Computes diff between sequences of strings.
*
* @param $from_lines array An array of strings.
* (Typically these are lines from a file.)
* @param $to_lines array An array of strings.
*/
public function __construct($from_lines, $to_lines) {
$eng = new DiffEngine();
$this->edits = $eng->diff($from_lines, $to_lines);
//$this->_check($from_lines, $to_lines);
}
/**
* Compute reversed Diff.
*
* SYNOPSIS:
*
* $diff = new Diff($lines1, $lines2);
* $rev = $diff->reverse();
* @return object A Diff object representing the inverse of the
* original diff.
*/
public function reverse() {
$rev = $this;
$rev->edits = array();
foreach ($this->edits as $edit) {
$rev->edits[] = $edit->reverse();
}
return $rev;
}
/**
* Check for empty diff.
*
* @return bool True iff two sequences were identical.
*/
public function isEmpty() {
foreach ($this->edits as $edit) {
if ($edit->type != 'copy') {
return FALSE;
}
}
return TRUE;
}
/**
* Compute the length of the Longest Common Subsequence (LCS).
*
* This is mostly for diagnostic purposed.
*
* @return int The length of the LCS.
*/
public function lcs() {
$lcs = 0;
foreach ($this->edits as $edit) {
if ($edit->type == 'copy') {
$lcs += sizeof($edit->orig);
}
}
return $lcs;
}
/**
* Gets the original set of lines.
*
* This reconstructs the $from_lines parameter passed to the
* constructor.
*
* @return array The original sequence of strings.
*/
public function orig() {
$lines = array();
foreach ($this->edits as $edit) {
if ($edit->orig) {
array_splice($lines, sizeof($lines), 0, $edit->orig);
}
}
return $lines;
}
/**
* Gets the closing set of lines.
*
* This reconstructs the $to_lines parameter passed to the
* constructor.
*
* @return array The sequence of strings.
*/
public function closing() {
$lines = array();
foreach ($this->edits as $edit) {
if ($edit->closing) {
array_splice($lines, sizeof($lines), 0, $edit->closing);
}
}
return $lines;
}
/**
* Check a Diff for validity.
*
* This is here only for debugging purposes.
*/
public function check($from_lines, $to_lines) {
if (serialize($from_lines) != serialize($this->orig())) {
trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
}
if (serialize($to_lines) != serialize($this->closing())) {
trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
}
$rev = $this->reverse();
if (serialize($to_lines) != serialize($rev->orig())) {
trigger_error("Reversed original doesn't match", E_USER_ERROR);
}
if (serialize($from_lines) != serialize($rev->closing())) {
trigger_error("Reversed closing doesn't match", E_USER_ERROR);
}
$prevtype = 'none';
foreach ($this->edits as $edit) {
if ( $prevtype == $edit->type ) {
trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
}
$prevtype = $edit->type;
}
$lcs = $this->lcs();
trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
}
/**
* Gets the list of differences as an array of diff operations.
*
* @return \Drupal\Component\Diff\Engine\DiffOp[]
* The list of differences as an array of diff operations.
*/
public function getEdits() {
return $this->edits;
}
}

View file

@ -0,0 +1,192 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\DiffFormatter.
*/
namespace Drupal\Component\Diff;
use Drupal\Component\Diff\Engine\DiffOpCopy;
/**
* A class to format Diffs
*
* This class formats the diff in classic diff format.
* It is intended that this class be customized via inheritance,
* to obtain fancier outputs.
* @todo document
* @private
* @subpackage DifferenceEngine
*/
class DiffFormatter {
/**
* Should a block header be shown?
*/
var $show_header = TRUE;
/**
* Number of leading context "lines" to preserve.
*
* This should be left at zero for this class, but subclasses
* may want to set this to other values.
*/
var $leading_context_lines = 0;
/**
* Number of trailing context "lines" to preserve.
*
* This should be left at zero for this class, but subclasses
* may want to set this to other values.
*/
var $trailing_context_lines = 0;
/**
* Format a diff.
*
* @param \Drupal\Component\Diff\Diff $diff
* A Diff object.
*
* @return string
* The formatted output.
*/
public function format(Diff $diff) {
$xi = $yi = 1;
$block = FALSE;
$context = array();
$nlead = $this->leading_context_lines;
$ntrail = $this->trailing_context_lines;
$this->_start_diff();
foreach ($diff->getEdits() as $edit) {
if ($edit->type == 'copy') {
if (is_array($block)) {
if (sizeof($edit->orig) <= $nlead + $ntrail) {
$block[] = $edit;
}
else {
if ($ntrail) {
$context = array_slice($edit->orig, 0, $ntrail);
$block[] = new DiffOpCopy($context);
}
$this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
$block = FALSE;
}
}
$context = $edit->orig;
}
else {
if (! is_array($block)) {
$context = array_slice($context, sizeof($context) - $nlead);
$x0 = $xi - sizeof($context);
$y0 = $yi - sizeof($context);
$block = array();
if ($context) {
$block[] = new DiffOpCopy($context);
}
}
$block[] = $edit;
}
if ($edit->orig) {
$xi += sizeof($edit->orig);
}
if ($edit->closing) {
$yi += sizeof($edit->closing);
}
}
if (is_array($block)) {
$this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
}
$end = $this->_end_diff();
if (!empty($xi)) {
$this->line_stats['counter']['x'] += $xi;
}
if (!empty($yi)) {
$this->line_stats['counter']['y'] += $yi;
}
return $end;
}
protected function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
$this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
foreach ($edits as $edit) {
if ($edit->type == 'copy') {
$this->_context($edit->orig);
}
elseif ($edit->type == 'add') {
$this->_added($edit->closing);
}
elseif ($edit->type == 'delete') {
$this->_deleted($edit->orig);
}
elseif ($edit->type == 'change') {
$this->_changed($edit->orig, $edit->closing);
}
else {
trigger_error('Unknown edit type', E_USER_ERROR);
}
}
$this->_end_block();
}
protected function _start_diff() {
ob_start();
}
protected function _end_diff() {
$val = ob_get_contents();
ob_end_clean();
return $val;
}
protected function _block_header($xbeg, $xlen, $ybeg, $ylen) {
if ($xlen > 1) {
$xbeg .= "," . ($xbeg + $xlen - 1);
}
if ($ylen > 1) {
$ybeg .= "," . ($ybeg + $ylen - 1);
}
return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
}
protected function _start_block($header) {
if ($this->show_header) {
echo $header . "\n";
}
}
protected function _end_block() {
}
protected function _lines($lines, $prefix = ' ') {
foreach ($lines as $line) {
echo "$prefix $line\n";
}
}
protected function _context($lines) {
$this->_lines($lines);
}
protected function _added($lines) {
$this->_lines($lines, '>');
}
protected function _deleted($lines) {
$this->_lines($lines, '<');
}
protected function _changed($orig, $closing) {
$this->_deleted($orig);
echo "---\n";
$this->_added($closing);
}
}

View file

@ -0,0 +1,461 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\Engine\DiffEngine.
*/
namespace Drupal\Component\Diff\Engine;
use Drupal\Component\Utility\Unicode;
/**
* Class used internally by Diff to actually compute the diffs.
*
* The algorithm used here is mostly lifted from the perl module
* Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
* http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
*
* More ideas are taken from:
* http://www.ics.uci.edu/~eppstein/161/960229.html
*
* Some ideas are (and a bit of code) are from from analyze.c, from GNU
* diffutils-2.7, which can be found at:
* ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
*
* closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
* are my own.
*
* Line length limits for robustness added by Tim Starling, 2005-08-31
*
* @author Geoffrey T. Dairiki, Tim Starling
* @private
* @subpackage DifferenceEngine
*/
class DiffEngine {
const USE_ASSERTS = FALSE;
const MAX_XREF_LENGTH = 10000;
public function diff($from_lines, $to_lines) {
$n_from = sizeof($from_lines);
$n_to = sizeof($to_lines);
$this->xchanged = $this->ychanged = array();
$this->xv = $this->yv = array();
$this->xind = $this->yind = array();
unset($this->seq);
unset($this->in_seq);
unset($this->lcs);
// Skip leading common lines.
for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
if ($from_lines[$skip] !== $to_lines[$skip]) {
break;
}
$this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
}
// Skip trailing common lines.
$xi = $n_from;
$yi = $n_to;
for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
if ($from_lines[$xi] !== $to_lines[$yi]) {
break;
}
$this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
}
// Ignore lines which do not exist in both files.
for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
$xhash[$this->_line_hash($from_lines[$xi])] = 1;
}
for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
$line = $to_lines[$yi];
if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
continue;
}
$yhash[$this->_line_hash($line)] = 1;
$this->yv[] = $line;
$this->yind[] = $yi;
}
for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
$line = $from_lines[$xi];
if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
continue;
}
$this->xv[] = $line;
$this->xind[] = $xi;
}
// Find the LCS.
$this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
// Merge edits when possible
$this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
$this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
// Compute the edit operations.
$edits = array();
$xi = $yi = 0;
while ($xi < $n_from || $yi < $n_to) {
$this::USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
$this::USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
// Skip matching "snake".
$copy = array();
while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
$copy[] = $from_lines[$xi++];
++$yi;
}
if ($copy) {
$edits[] = new DiffOpCopy($copy);
}
// Find deletes & adds.
$delete = array();
while ($xi < $n_from && $this->xchanged[$xi]) {
$delete[] = $from_lines[$xi++];
}
$add = array();
while ($yi < $n_to && $this->ychanged[$yi]) {
$add[] = $to_lines[$yi++];
}
if ($delete && $add) {
$edits[] = new DiffOpChange($delete, $add);
}
elseif ($delete) {
$edits[] = new DiffOpDelete($delete);
}
elseif ($add) {
$edits[] = new DiffOpAdd($add);
}
}
return $edits;
}
/**
* Returns the whole line if it's small enough, or the MD5 hash otherwise.
*/
protected function _line_hash($line) {
if (Unicode::strlen($line) > $this::MAX_XREF_LENGTH) {
return md5($line);
}
else {
return $line;
}
}
/**
* Divide the Largest Common Subsequence (LCS) of the sequences
* [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
* sized segments.
*
* Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
* array of NCHUNKS+1 (X, Y) indexes giving the diving points between
* sub sequences. The first sub-sequence is contained in [X0, X1),
* [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
* that (X0, Y0) == (XOFF, YOFF) and
* (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
*
* This function assumes that the first lines of the specified portions
* of the two files do not match, and likewise that the last lines do not
* match. The caller must trim matching lines from the beginning and end
* of the portions it is going to specify.
*/
protected function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
$flip = FALSE;
if ($xlim - $xoff > $ylim - $yoff) {
// Things seems faster (I'm not sure I understand why)
// when the shortest sequence in X.
$flip = TRUE;
list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
}
if ($flip) {
for ($i = $ylim - 1; $i >= $yoff; $i--) {
$ymatches[$this->xv[$i]][] = $i;
}
}
else {
for ($i = $ylim - 1; $i >= $yoff; $i--) {
$ymatches[$this->yv[$i]][] = $i;
}
}
$this->lcs = 0;
$this->seq[0]= $yoff - 1;
$this->in_seq = array();
$ymids[0] = array();
$numer = $xlim - $xoff + $nchunks - 1;
$x = $xoff;
for ($chunk = 0; $chunk < $nchunks; $chunk++) {
if ($chunk > 0) {
for ($i = 0; $i <= $this->lcs; $i++) {
$ymids[$i][$chunk-1] = $this->seq[$i];
}
}
$x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
for ( ; $x < $x1; $x++) {
$line = $flip ? $this->yv[$x] : $this->xv[$x];
if (empty($ymatches[$line])) {
continue;
}
$matches = $ymatches[$line];
reset($matches);
while (list ($junk, $y) = each($matches)) {
if (empty($this->in_seq[$y])) {
$k = $this->_lcs_pos($y);
$this::USE_ASSERTS && assert($k > 0);
$ymids[$k] = $ymids[$k-1];
break;
}
}
while (list ($junk, $y) = each($matches)) {
if ($y > $this->seq[$k-1]) {
$this::USE_ASSERTS && assert($y < $this->seq[$k]);
// Optimization: this is a common case:
// next match is just replacing previous match.
$this->in_seq[$this->seq[$k]] = FALSE;
$this->seq[$k] = $y;
$this->in_seq[$y] = 1;
}
elseif (empty($this->in_seq[$y])) {
$k = $this->_lcs_pos($y);
$this::USE_ASSERTS && assert($k > 0);
$ymids[$k] = $ymids[$k-1];
}
}
}
}
$seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
$ymid = $ymids[$this->lcs];
for ($n = 0; $n < $nchunks - 1; $n++) {
$x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
$y1 = $ymid[$n] + 1;
$seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
}
$seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
return array($this->lcs, $seps);
}
protected function _lcs_pos($ypos) {
$end = $this->lcs;
if ($end == 0 || $ypos > $this->seq[$end]) {
$this->seq[++$this->lcs] = $ypos;
$this->in_seq[$ypos] = 1;
return $this->lcs;
}
$beg = 1;
while ($beg < $end) {
$mid = (int)(($beg + $end) / 2);
if ($ypos > $this->seq[$mid]) {
$beg = $mid + 1;
}
else {
$end = $mid;
}
}
$this::USE_ASSERTS && assert($ypos != $this->seq[$end]);
$this->in_seq[$this->seq[$end]] = FALSE;
$this->seq[$end] = $ypos;
$this->in_seq[$ypos] = 1;
return $end;
}
/**
* Find LCS of two sequences.
*
* The results are recorded in the vectors $this->{x,y}changed[], by
* storing a 1 in the element for each line that is an insertion
* or deletion (ie. is not in the LCS).
*
* The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
*
* Note that XLIM, YLIM are exclusive bounds.
* All line numbers are origin-0 and discarded lines are not counted.
*/
protected function _compareseq($xoff, $xlim, $yoff, $ylim) {
// Slide down the bottom initial diagonal.
while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
++$xoff;
++$yoff;
}
// Slide up the top initial diagonal.
while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
--$xlim;
--$ylim;
}
if ($xoff == $xlim || $yoff == $ylim) {
$lcs = 0;
}
else {
// This is ad hoc but seems to work well.
//$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
//$nchunks = max(2, min(8, (int)$nchunks));
$nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
list($lcs, $seps)
= $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
}
if ($lcs == 0) {
// X and Y sequences have no common subsequence:
// mark all changed.
while ($yoff < $ylim) {
$this->ychanged[$this->yind[$yoff++]] = 1;
}
while ($xoff < $xlim) {
$this->xchanged[$this->xind[$xoff++]] = 1;
}
}
else {
// Use the partitions to split this problem into subproblems.
reset($seps);
$pt1 = $seps[0];
while ($pt2 = next($seps)) {
$this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
$pt1 = $pt2;
}
}
}
/**
* Adjust inserts/deletes of identical lines to join changes
* as much as possible.
*
* We do something when a run of changed lines include a
* line at one end and has an excluded, identical line at the other.
* We are free to choose which identical line is included.
* `compareseq' usually chooses the one at the beginning,
* but usually it is cleaner to consider the following identical line
* to be the "change".
*
* This is extracted verbatim from analyze.c (GNU diffutils-2.7).
*/
protected function _shift_boundaries($lines, &$changed, $other_changed) {
$i = 0;
$j = 0;
$this::USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
$len = sizeof($lines);
$other_len = sizeof($other_changed);
while (1) {
/*
* Scan forwards to find beginning of another run of changes.
* Also keep track of the corresponding point in the other file.
*
* Throughout this code, $i and $j are adjusted together so that
* the first $i elements of $changed and the first $j elements
* of $other_changed both contain the same number of zeros
* (unchanged lines).
* Furthermore, $j is always kept so that $j == $other_len or
* $other_changed[$j] == FALSE.
*/
while ($j < $other_len && $other_changed[$j]) {
$j++;
}
while ($i < $len && ! $changed[$i]) {
$this::USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
$i++;
$j++;
while ($j < $other_len && $other_changed[$j]) {
$j++;
}
}
if ($i == $len) {
break;
}
$start = $i;
// Find the end of this run of changes.
while (++$i < $len && $changed[$i]) {
continue;
}
do {
/*
* Record the length of this run of changes, so that
* we can later determine whether the run has grown.
*/
$runlength = $i - $start;
/*
* Move the changed region back, so long as the
* previous unchanged line matches the last changed one.
* This merges with previous changed regions.
*/
while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
$changed[--$start] = 1;
$changed[--$i] = FALSE;
while ($start > 0 && $changed[$start - 1]) {
$start--;
}
$this::USE_ASSERTS && assert('$j > 0');
while ($other_changed[--$j]) {
continue;
}
$this::USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
}
/*
* Set CORRESPONDING to the end of the changed run, at the last
* point where it corresponds to a changed run in the other file.
* CORRESPONDING == LEN means no such point has been found.
*/
$corresponding = $j < $other_len ? $i : $len;
/*
* Move the changed region forward, so long as the
* first changed line matches the following unchanged one.
* This merges with following changed regions.
* Do this second, so that if there are no merges,
* the changed region is moved forward as far as possible.
*/
while ($i < $len && $lines[$start] == $lines[$i]) {
$changed[$start++] = FALSE;
$changed[$i++] = 1;
while ($i < $len && $changed[$i]) {
$i++;
}
$this::USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
$j++;
if ($j < $other_len && $other_changed[$j]) {
$corresponding = $i;
while ($j < $other_len && $other_changed[$j]) {
$j++;
}
}
}
} while ($runlength != $i - $start);
/*
* If possible, move the fully-merged run of changes
* back to a corresponding run in the other file.
*/
while ($corresponding < $i) {
$changed[--$start] = 1;
$changed[--$i] = 0;
$this::USE_ASSERTS && assert('$j > 0');
while ($other_changed[--$j]) {
continue;
}
$this::USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
}
}
}
}

View file

@ -0,0 +1,31 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\Engine\DiffOp.
*/
namespace Drupal\Component\Diff\Engine;
/**
* @todo document
* @private
* @subpackage DifferenceEngine
*/
class DiffOp {
var $type;
var $orig;
var $closing;
public function reverse() {
trigger_error('pure virtual', E_USER_ERROR);
}
public function norig() {
return $this->orig ? sizeof($this->orig) : 0;
}
public function nclosing() {
return $this->closing ? sizeof($this->closing) : 0;
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\Engine\DiffOpAdd.
*/
namespace Drupal\Component\Diff\Engine;
/**
* @todo document
* @private
* @subpackage DifferenceEngine
*/
class DiffOpAdd extends DiffOp {
var $type = 'add';
public function __construct($lines) {
$this->closing = $lines;
$this->orig = FALSE;
}
public function reverse() {
return new DiffOpDelete($this->closing);
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\Engine\DiffOpChange.
*/
namespace Drupal\Component\Diff\Engine;
/**
* @todo document
* @private
* @subpackage DifferenceEngine
*/
class DiffOpChange extends DiffOp {
var $type = 'change';
public function __construct($orig, $closing) {
$this->orig = $orig;
$this->closing = $closing;
}
public function reverse() {
return new DiffOpChange($this->closing, $this->orig);
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\Engine\DiffOpCopy.
*/
namespace Drupal\Component\Diff\Engine;
/**
* @todo document
* @private
* @subpackage DifferenceEngine
*/
class DiffOpCopy extends DiffOp {
var $type = 'copy';
public function __construct($orig, $closing = FALSE) {
if (!is_array($closing)) {
$closing = $orig;
}
$this->orig = $orig;
$this->closing = $closing;
}
public function reverse() {
return new DiffOpCopy($this->closing, $this->orig);
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\Engine\DiffOpDelete.
*/
namespace Drupal\Component\Diff\Engine;
/**
* @todo document
* @private
* @subpackage DifferenceEngine
*/
class DiffOpDelete extends DiffOp {
var $type = 'delete';
public function __construct($lines) {
$this->orig = $lines;
$this->closing = FALSE;
}
public function reverse() {
return new DiffOpAdd($this->orig);
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\Engine\HWLDFWordAccumulator.
*/
namespace Drupal\Component\Diff\Engine;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\SafeMarkup;
/**
* Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
*
*/
/**
* @todo document
* @private
* @subpackage DifferenceEngine
*/
class HWLDFWordAccumulator {
/**
* An iso-8859-x non-breaking space.
*/
const NBSP = '&#160;';
protected $lines = array();
protected $line = '';
protected $group = '';
protected $tag = '';
protected function _flushGroup($new_tag) {
if ($this->group !== '') {
if ($this->tag == 'mark') {
$this->line = SafeMarkup::format('@original_line<span class="diffchange">@group</span>', ['@original_line' => $this->line, '@group' => $this->group]);
}
else {
$this->line = SafeMarkup::format('@original_line@group', ['@original_line' => $this->line, '@group' => $this->group]);
}
}
$this->group = '';
$this->tag = $new_tag;
}
protected function _flushLine($new_tag) {
$this->_flushGroup($new_tag);
if ($this->line != '') {
array_push($this->lines, $this->line);
}
else {
// make empty lines visible by inserting an NBSP
array_push($this->lines, $this::NBSP);
}
$this->line = '';
}
public function addWords($words, $tag = '') {
if ($tag != $this->tag) {
$this->_flushGroup($tag);
}
foreach ($words as $word) {
// new-line should only come as first char of word.
if ($word == '') {
continue;
}
if ($word[0] == "\n") {
$this->_flushLine($tag);
$word = Unicode::substr($word, 1);
}
assert(!strstr($word, "\n"));
$this->group .= $word;
}
}
public function getLines() {
$this->_flushLine('~done');
return $this->lines;
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\MappedDiff.
*/
namespace Drupal\Component\Diff;
/**
* FIXME: bad name.
* @todo document
* @private
* @subpackage DifferenceEngine
*/
class MappedDiff extends Diff {
/**
* Constructor.
*
* Computes diff between sequences of strings.
*
* This can be used to compute things like
* case-insensitive diffs, or diffs which ignore
* changes in white-space.
*
* @param $from_lines array An array of strings.
* (Typically these are lines from a file.)
*
* @param $to_lines array An array of strings.
*
* @param $mapped_from_lines array This array should
* have the same size number of elements as $from_lines.
* The elements in $mapped_from_lines and
* $mapped_to_lines are what is actually compared
* when computing the diff.
*
* @param $mapped_to_lines array This array should
* have the same number of elements as $to_lines.
*/
public function __construct($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
assert(sizeof($from_lines) == sizeof($mapped_from_lines));
assert(sizeof($to_lines) == sizeof($mapped_to_lines));
parent::__construct($mapped_from_lines, $mapped_to_lines);
$xi = $yi = 0;
for ($i = 0; $i < sizeof($this->edits); $i++) {
$orig = &$this->edits[$i]->orig;
if (is_array($orig)) {
$orig = array_slice($from_lines, $xi, sizeof($orig));
$xi += sizeof($orig);
}
$closing = &$this->edits[$i]->closing;
if (is_array($closing)) {
$closing = array_slice($to_lines, $yi, sizeof($closing));
$yi += sizeof($closing);
}
}
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* @file
* Contains \Drupal\Component\Diff\WordLevelDiff.
*/
namespace Drupal\Component\Diff;
use Drupal\Component\Diff\Engine\HWLDFWordAccumulator;
use Drupal\Component\Utility\Unicode;
/**
* @todo document
* @private
* @subpackage DifferenceEngine
*/
class WordLevelDiff extends MappedDiff {
const MAX_LINE_LENGTH = 10000;
public function __construct($orig_lines, $closing_lines) {
list($orig_words, $orig_stripped) = $this->_split($orig_lines);
list($closing_words, $closing_stripped) = $this->_split($closing_lines);
parent::__construct($orig_words, $closing_words, $orig_stripped, $closing_stripped);
}
protected function _split($lines) {
$words = array();
$stripped = array();
$first = TRUE;
foreach ($lines as $line) {
// If the line is too long, just pretend the entire line is one big word
// This prevents resource exhaustion problems
if ( $first ) {
$first = FALSE;
}
else {
$words[] = "\n";
$stripped[] = "\n";
}
if (Unicode::strlen($line) > $this::MAX_LINE_LENGTH) {
$words[] = $line;
$stripped[] = $line;
}
else {
if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
$words = array_merge($words, $m[0]);
$stripped = array_merge($stripped, $m[1]);
}
}
}
return array($words, $stripped);
}
public function orig() {
$orig = new HWLDFWordAccumulator();
foreach ($this->edits as $edit) {
if ($edit->type == 'copy') {
$orig->addWords($edit->orig);
}
elseif ($edit->orig) {
$orig->addWords($edit->orig, 'mark');
}
}
$lines = $orig->getLines();
return $lines;
}
public function closing() {
$closing = new HWLDFWordAccumulator();
foreach ($this->edits as $edit) {
if ($edit->type == 'copy') {
$closing->addWords($edit->closing);
}
elseif ($edit->closing) {
$closing->addWords($edit->closing, 'mark');
}
}
$lines = $closing->getLines();
return $lines;
}
}

View file

@ -0,0 +1,23 @@
<?php
/**
* @file
* Contains \Drupal\Component\Discovery\DiscoverableInterface.
*/
namespace Drupal\Component\Discovery;
/**
* Interface for classes providing a type of discovery.
*/
interface DiscoverableInterface {
/**
* Returns an array of discoverable items.
*
* @return array
* An array of discovered data keyed by provider.
*/
public function findAll();
}

View file

@ -0,0 +1,94 @@
<?php
/**
* @file
* Contains \Drupal\Component\Discovery\YamlDiscovery.
*/
namespace Drupal\Component\Discovery;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\FileCache\FileCacheFactory;
/**
* Provides discovery for YAML files within a given set of directories.
*/
class YamlDiscovery implements DiscoverableInterface {
/**
* The base filename to look for in each directory.
*
* @var string
*/
protected $name;
/**
* An array of directories to scan, keyed by the provider.
*
* @var array
*/
protected $directories = array();
/**
* Constructs a YamlDiscovery object.
*
* @param string $name
* The base filename to look for in each directory. The format will be
* $provider.$name.yml.
* @param array $directories
* An array of directories to scan, keyed by the provider.
*/
public function __construct($name, array $directories) {
$this->name = $name;
$this->directories = $directories;
}
/**
* {@inheritdoc}
*/
public function findAll() {
$all = array();
$files = $this->findFiles();
$provider_by_files = array_flip($files);
$file_cache = FileCacheFactory::get('yaml_discovery:' . $this->name);
// Try to load from the file cache first.
foreach ($file_cache->getMultiple($files) as $file => $data) {
$all[$provider_by_files[$file]] = $data;
unset($provider_by_files[$file]);
}
// If there are files left that were not returned from the cache, load and
// parse them now. This list was flipped above and is keyed by filename.
if ($provider_by_files) {
foreach ($provider_by_files as $file => $provider) {
// If a file is empty or its contents are commented out, return an empty
// array instead of NULL for type consistency.
$all[$provider] = Yaml::decode(file_get_contents($file)) ?: [];
$file_cache->set($file, $all[$provider]);
}
}
return $all;
}
/**
* Returns an array of file paths, keyed by provider.
*
* @return array
*/
protected function findFiles() {
$files = array();
foreach ($this->directories as $provider => $directory) {
$file = $directory . '/' . $provider . '.' . $this->name . '.yml';
if (file_exists($file)) {
$files[$provider] = $file;
}
}
return $files;
}
}

View file

@ -0,0 +1,238 @@
<?php
/**
* @file
* Contains \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher.
*/
namespace Drupal\Component\EventDispatcher;
use Symfony\Component\DependencyInjection\IntrospectableContainerInterface;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* A performance optimized container aware event dispatcher.
*
* This version of the event dispatcher contains the following optimizations
* in comparison to the Symfony event dispatcher component:
*
* <dl>
* <dt>Faster instantiation of the event dispatcher service</dt>
* <dd>
* Instead of calling <code>addSubscriberService</code> once for each
* subscriber, a precompiled array of listener definitions is passed
* directly to the constructor. This is faster by roughly an order of
* magnitude. The listeners are collected and prepared using a compiler
* pass.
* </dd>
* <dt>Lazy instantiation of listeners</dt>
* <dd>
* Services are only retrieved from the container just before invocation.
* Especially when dispatching the KernelEvents::REQUEST event, this leads
* to a more timely invocation of the first listener. Overall dispatch
* runtime is not affected by this change though.
* </dd>
* </dl>
*/
class ContainerAwareEventDispatcher implements EventDispatcherInterface {
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\IntrospectableContainerInterface;
*/
protected $container;
/**
* Listener definitions.
*
* A nested array of listener definitions keyed by event name and priority.
* A listener definition is an associative array with one of the following key
* value pairs:
* - callable: A callable listener
* - service: An array of the form [service id, method]
*
* A service entry will be resolved to a callable only just before its
* invocation.
*
* @var array
*/
protected $listeners;
/**
* Whether listeners need to be sorted prior to dispatch, keyed by event name.
*
* @var TRUE[]
*/
protected $unsorted;
/**
* Constructs a container aware event dispatcher.
*
* @param \Symfony\Component\EventDispatcher\IntrospectableContainerInterface $container
* The service container.
* @param array $listeners
* A nested array of listener definitions keyed by event name and priority.
* The array is expected to be ordered by priority. A listener definition is
* an associative array with one of the following key value pairs:
* - callable: A callable listener
* - service: An array of the form [service id, method]
* A service entry will be resolved to a callable only just before its
* invocation.
*/
public function __construct(IntrospectableContainerInterface $container, array $listeners = []) {
$this->container = $container;
$this->listeners = $listeners;
$this->unsorted = [];
}
/**
* {@inheritdoc}
*/
public function dispatch($event_name, Event $event = NULL) {
if ($event === NULL) {
$event = new Event();
}
$event->setDispatcher($this);
$event->setName($event_name);
if (isset($this->listeners[$event_name])) {
// Sort listeners if necessary.
if (isset($this->unsorted[$event_name])) {
krsort($this->listeners[$event_name]);
unset($this->unsorted[$event_name]);
}
// Invoke listeners and resolve callables if necessary.
foreach ($this->listeners[$event_name] as $priority => &$definitions) {
foreach ($definitions as $key => &$definition) {
if (!isset($definition['callable'])) {
$definition['callable'] = [$this->container->get($definition['service'][0]), $definition['service'][1]];
}
$definition['callable']($event, $event_name, $this);
if ($event->isPropagationStopped()) {
return $event;
}
}
}
}
return $event;
}
/**
* {@inheritdoc}
*/
public function getListeners($event_name = NULL) {
$result = [];
if ($event_name === NULL) {
// If event name was omitted, collect all listeners of all events.
foreach (array_keys($this->listeners) as $event_name) {
$listeners = $this->getListeners($event_name);
if (!empty($listeners)) {
$result[$event_name] = $listeners;
}
}
}
elseif (isset($this->listeners[$event_name])) {
// Sort listeners if necessary.
if (isset($this->unsorted[$event_name])) {
krsort($this->listeners[$event_name]);
unset($this->unsorted[$event_name]);
}
// Collect listeners and resolve callables if necessary.
foreach ($this->listeners[$event_name] as $priority => &$definitions) {
foreach ($definitions as $key => &$definition) {
if (!isset($definition['callable'])) {
$definition['callable'] = [$this->container->get($definition['service'][0]), $definition['service'][1]];
}
$result[] = $definition['callable'];
}
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function hasListeners($event_name = NULL) {
return (bool) count($this->getListeners($event_name));
}
/**
* {@inheritdoc}
*/
public function addListener($event_name, $listener, $priority = 0) {
$this->listeners[$event_name][$priority][] = ['callable' => $listener];
$this->unsorted[$event_name] = TRUE;
}
/**
* {@inheritdoc}
*/
public function removeListener($event_name, $listener) {
if (!isset($this->listeners[$event_name])) {
return;
}
foreach ($this->listeners[$event_name] as $priority => $definitions) {
foreach ($definitions as $key => $definition) {
if (!isset($definition['callable'])) {
if (!$this->container->initialized($definition['service'][0])) {
continue;
}
$definition['callable'] = [$this->container->get($definition['service'][0]), $definition['service'][1]];
}
if ($definition['callable'] === $listener) {
unset($this->listeners[$event_name][$priority][$key]);
}
}
}
}
/**
* {@inheritdoc}
*/
public function addSubscriber(EventSubscriberInterface $subscriber) {
foreach ($subscriber->getSubscribedEvents() as $event_name => $params) {
if (is_string($params)) {
$this->addListener($event_name, array($subscriber, $params));
}
elseif (is_string($params[0])) {
$this->addListener($event_name, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0);
}
else {
foreach ($params as $listener) {
$this->addListener($event_name, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0);
}
}
}
}
/**
* {@inheritdoc}
*/
public function removeSubscriber(EventSubscriberInterface $subscriber) {
foreach ($subscriber->getSubscribedEvents() as $event_name => $params) {
if (is_array($params) && is_array($params[0])) {
foreach ($params as $listener) {
$this->removeListener($event_name, array($subscriber, $listener[0]));
}
}
else {
$this->removeListener($event_name, array($subscriber, is_string($params) ? $params : $params[0]));
}
}
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\ApcuFileCacheBackend.
*/
namespace Drupal\Component\FileCache;
/**
* APCu backend for the file cache.
*/
class ApcuFileCacheBackend implements FileCacheBackendInterface {
/**
* {@inheritdoc}
*/
public function fetch(array $cids) {
return apc_fetch($cids);
}
/**
* {@inheritdoc}
*/
public function store($cid, $data) {
apc_store($cid, $data);
}
/**
* {@inheritdoc}
*/
public function delete($cid) {
apc_delete($cid);
}
}

View file

@ -0,0 +1,156 @@
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\FileCache.
*/
namespace Drupal\Component\FileCache;
/**
* Allows to cache data based on file modification dates.
*/
class FileCache implements FileCacheInterface {
/**
* Prefix that is used for cache entries.
*
* @var string
*/
protected $prefix;
/**
* Static cache that contains already loaded cache entries.
*
* @var array
*/
protected static $cached = [];
/**
* The collection identifier of this cache.
*
* @var string
*/
protected $collection;
/**
* The cache backend backing this FileCache object.
*
* @var \Drupal\Component\FileCache\FileCacheBackendInterface
*/
protected $cache;
/**
* Constructs a FileCache object.
*
* @param string $prefix
* The cache prefix.
* @param string $collection
* A collection identifier to ensure that the same files could be cached for
* different purposes without clashing.
* @param string|null $cache_backend_class
* (optional) The class that should be used as cache backend.
* @param array $cache_backend_configuration
* (optional) The configuration for the backend class.
*/
public function __construct($prefix, $collection, $cache_backend_class = NULL, array $cache_backend_configuration = []) {
if (empty($prefix)) {
throw new \InvalidArgumentException('Required prefix configuration is missing');
}
$this->prefix = $prefix;
$this->collection = $collection;
if (isset($cache_backend_class)) {
$this->cache = new $cache_backend_class($cache_backend_configuration);
}
}
/**
* {@inheritdoc}
*/
public function get($filepath) {
$filepaths = [$filepath];
$cached = $this->getMultiple($filepaths);
return isset($cached[$filepath]) ? $cached[$filepath] : NULL;
}
/**
* {@inheritdoc}
*/
public function getMultiple(array $filepaths) {
$file_data = [];
$remaining_cids = [];
// First load from the static cache what we can.
foreach ($filepaths as $filepath) {
if (!file_exists($filepath)) {
continue;
}
$realpath = realpath($filepath);
// If the file exists but realpath returns nothing, it is using a stream
// wrapper, those are not supported.
if (empty($realpath)) {
continue;
}
$cid = $this->prefix . ':' . $this->collection . ':' . $realpath;
if (isset(static::$cached[$cid]) && static::$cached[$cid]['mtime'] == filemtime($filepath)) {
$file_data[$filepath] = static::$cached[$cid]['data'];
}
else {
// Collect a list of cache IDs that we still need to fetch from cache
// backend.
$remaining_cids[$cid] = $filepath;
}
}
// If there are any cache IDs left to fetch from the cache backend.
if ($remaining_cids && $this->cache) {
$cache_results = $this->cache->fetch(array_keys($remaining_cids)) ?: [];
foreach ($cache_results as $cid => $cached) {
$filepath = $remaining_cids[$cid];
if ($cached['mtime'] == filemtime($filepath)) {
$file_data[$cached['filepath']] = $cached['data'];
static::$cached[$cid] = $cached;
}
}
}
return $file_data;
}
/**
* {@inheritdoc}
*/
public function set($filepath, $data) {
$realpath = realpath($filepath);
$cached = [
'mtime' => filemtime($filepath),
'filepath' => $filepath,
'data' => $data,
];
$cid = $this->prefix . ':' . $this->collection . ':' . $realpath;
static::$cached[$cid] = $cached;
if ($this->cache) {
$this->cache->store($cid, $cached);
}
}
/**
* {@inheritdoc}
*/
public function delete($filepath) {
$realpath = realpath($filepath);
$cid = $this->prefix . ':' . $this->collection . ':' . $realpath;
unset(static::$cached[$cid]);
if ($this->cache) {
$this->cache->delete($cid);
}
}
}

View file

@ -0,0 +1,44 @@
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\FileCacheBackendInterface.
*/
namespace Drupal\Component\FileCache;
/**
* Defines an interface inspired by APCu for FileCache backends.
*/
interface FileCacheBackendInterface {
/**
* Fetches data from the cache backend.
*
* @param array $cids
* The cache IDs to fetch.
*
* @return array
* An array containing cache entries keyed by cache ID.
*/
public function fetch(array $cids);
/**
* Stores data into a cache backend.
*
* @param string $cid
* The cache ID to store data to.
* @param mixed $data
* The data to store.
*/
public function store($cid, $data);
/**
* Deletes data from a cache backend.
*
* @param string $cid
* The cache ID to delete.
*/
public function delete($cid);
}

View file

@ -0,0 +1,107 @@
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\FileCacheFactory.
*/
namespace Drupal\Component\FileCache;
/**
* Creates a FileCache object.
*/
class FileCacheFactory {
/**
* The configuration used to create FileCache objects.
*
* @var array $configuration
*/
protected static $configuration;
/**
* The cache prefix.
*
* @var string
*/
protected static $prefix;
/**
* Instantiates a FileCache object for a given collection identifier.
*
* @param string $collection
* The collection identifier for this FileCache.
* @param array $default_configuration
* (optional) The default configuration for this FileCache collection. This
* can be used to e.g. specify default usage of a FileCache class.
*
* @return \Drupal\Component\FileCache\FileCacheInterface
* The initialized FileCache object.
*/
public static function get($collection, $default_configuration = []) {
$default_configuration += [
'class' => '\Drupal\Component\FileCache\FileCache',
'collection' => $collection,
'cache_backend_class' => NULL,
'cache_backend_configuration' => [],
];
$configuration = [];
if (isset(static::$configuration[$collection])) {
$configuration = static::$configuration[$collection];
}
elseif (isset(static::$configuration['default'])) {
$configuration = static::$configuration['default'];
}
// Add defaults to the configuration.
$configuration = $configuration + $default_configuration;
$class = $configuration['class'];
return new $class(static::getPrefix(), $configuration['collection'], $configuration['cache_backend_class'], $configuration['cache_backend_configuration']);
}
/**
* Gets the configuration used for constructing future file cache objects.
*
* @return array
* The configuration that is used.
*/
public static function getConfiguration() {
return static::$configuration;
}
/**
* Sets the configuration to use for constructing future file cache objects.
*
* @param array $configuration
* The configuration to use.
*/
public static function setConfiguration($configuration) {
static::$configuration = $configuration;
}
/**
* Returns the cache prefix.
*
* @return string
* The cache prefix.
*/
public static function getPrefix() {
return static::$prefix;
}
/**
* Sets the cache prefix that should be used.
*
* Should be set to a secure, unique key to prevent cache pollution by a
* third party.
*
* @param string $prefix
* The cache prefix.
*/
public static function setPrefix($prefix) {
static::$prefix = $prefix;
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\FileCacheInterface.
*/
namespace Drupal\Component\FileCache;
/**
* Interface for objects that allow caching file data.
*
* Parsing YAML, annotations or similar data out of files can be a
* time-consuming process, especially since those files usually don't change
* and identical data is parsed over and over again.
*
* File cache is a self-contained caching layer for such processing, that relies
* on the file modification to ensure that cached data is still up to date and
* does not need to be invalidated externally.
*/
interface FileCacheInterface {
/**
* Gets data based on a filename.
*
* @param string $filepath
* Path of the file that the cached data is based on.
*
* @return mixed|null
* The data that was persisted with set() or NULL if there is no data
* or the file has been modified.
*/
public function get($filepath);
/**
* Gets data based on filenames.
*
* @param string[] $filepaths
* List of file paths used as cache identifiers.
*
* @return array
* List of cached data keyed by the passed in file paths.
*/
public function getMultiple(array $filepaths);
/**
* Stores data based on a filename.
*
* @param string $filepath
* Path of the file that the cached data is based on.
* @param mixed $data
* The data that should be cached.
*/
public function set($filepath, $data);
/**
* Deletes data from the cache.
*
* @param string $filepath
* Path of the file that the cached data is based on.
*/
public function delete($filepath);
}

View file

@ -0,0 +1,58 @@
<?php
/**
* @file
* Contains \Drupal\Component\FileCache\NullFileCache.
*/
namespace Drupal\Component\FileCache;
/**
* Null implementation for the file cache.
*/
class NullFileCache implements FileCacheInterface {
/**
* Constructs a FileCache object.
*
* @param string $prefix
* A prefix that is used as a prefix, should be set to a secure, unique key
* to prevent cache pollution by a third party.
* @param string $collection
* A collection identifier to ensure that the same files could be cached for
* different purposes without clashing.
* @param string|null $cache_backend_class
* (optional) The class that should be used as cache backend.
* @param array $cache_backend_configuration
* (optional) The configuration for the backend class.
*/
public function __construct($prefix, $collection, $cache_backend_class = NULL, array $cache_backend_configuration = []) {
}
/**
* {@inheritdoc}
*/
public function get($filepath) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getMultiple(array $filepaths) {
return [];
}
/**
* {@inheritdoc}
*/
public function set($filepath, $data) {
}
/**
* {@inheritdoc}
*/
public function delete($filepath) {
}
}

View file

@ -0,0 +1,567 @@
<?php
/**
* @file
* Contains \Drupal\Component\Gettext\PoHeader.
*/
namespace Drupal\Component\Gettext;
/**
* Gettext PO header handler.
*
* Possible Gettext PO header elements are explained in
* http://www.gnu.org/software/gettext/manual/gettext.html#Header-Entry,
* but we only support a subset of these directly.
*
* Example header:
*
* "Project-Id-Version: Drupal core (7.11)\n"
* "POT-Creation-Date: 2012-02-12 22:59+0000\n"
* "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
* "Language-Team: Catalan\n"
* "MIME-Version: 1.0\n"
* "Content-Type: text/plain; charset=utf-8\n"
* "Content-Transfer-Encoding: 8bit\n"
* "Plural-Forms: nplurals=2; plural=(n>1);\n"
*/
class PoHeader {
/**
* Language code.
*
* @var string
*/
private $_langcode;
/**
* Formula for the plural form.
*
* @var string
*/
private $_pluralForms;
/**
* Author(s) of the file.
*
* @var string
*/
private $_authors;
/**
* Date the po file got created.
*
* @var string
*/
private $_po_date;
/**
* Human readable language name.
*
* @var string
*/
private $_languageName;
/**
* Name of the project the translation belongs to.
*
* @var string
*/
private $_projectName;
/**
* Constructor, creates a PoHeader with default values.
*
* @param string $langcode
* Language code.
*/
public function __construct($langcode = NULL) {
$this->_langcode = $langcode;
// Ignore errors when run during site installation before
// date_default_timezone_set() is called.
$this->_po_date = @date("Y-m-d H:iO");
$this->_pluralForms = 'nplurals=2; plural=(n > 1);';
}
/**
* Gets the plural form.
*
* @return string
* Plural form component from the header, for example:
* 'nplurals=2; plural=(n > 1);'.
*/
function getPluralForms() {
return $this->_pluralForms;
}
/**
* Set the human readable language name.
*
* @param string $languageName
* Human readable language name.
*/
function setLanguageName($languageName) {
$this->_languageName = $languageName;
}
/**
* Gets the human readable language name.
*
* @return string
* The human readable language name.
*/
function getLanguageName() {
return $this->_languageName;
}
/**
* Set the project name.
*
* @param string $projectName
* Human readable project name.
*/
function setProjectName($projectName) {
$this->_projectName = $projectName;
}
/**
* Gets the project name.
*
* @return string
* The human readable project name.
*/
function getProjectName() {
return $this->_projectName;
}
/**
* Populate internal values from a string.
*
* @param string $header
* Full header string with key-value pairs.
*/
public function setFromString($header) {
// Get an array of all header values for processing.
$values = $this->parseHeader($header);
// There is only one value relevant for our header implementation when
// reading, and that is the plural formula.
if (!empty($values['Plural-Forms'])) {
$this->_pluralForms = $values['Plural-Forms'];
}
}
/**
* Generate a Gettext PO formatted header string based on data set earlier.
*/
public function __toString() {
$output = '';
$isTemplate = empty($this->_languageName);
$output .= '# ' . ($isTemplate ? 'LANGUAGE' : $this->_languageName) . ' translation of ' . ($isTemplate ? 'PROJECT' : $this->_projectName) . "\n";
if (!empty($this->_authors)) {
$output .= '# Generated by ' . implode("\n# ", $this->_authors) . "\n";
}
$output .= "#\n";
// Add the actual header information.
$output .= "msgid \"\"\n";
$output .= "msgstr \"\"\n";
$output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
$output .= "\"POT-Creation-Date: " . $this->_po_date . "\\n\"\n";
$output .= "\"PO-Revision-Date: " . $this->_po_date . "\\n\"\n";
$output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
$output .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
$output .= "\"MIME-Version: 1.0\\n\"\n";
$output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
$output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
$output .= "\"Plural-Forms: " . $this->_pluralForms . "\\n\"\n";
$output .= "\n";
return $output;
}
/**
* Parses a Plural-Forms entry from a Gettext Portable Object file header.
*
* @param string $pluralforms
* The Plural-Forms entry value.
*
* @return
* An indexed array of parsed plural formula data. Containing:
* - 'nplurals': The number of plural forms defined by the plural formula.
* - 'plurals': Array of plural positions keyed by plural value.
*
* @throws Exception
*/
function parsePluralForms($pluralforms) {
$plurals = array();
// First, delete all whitespace.
$pluralforms = strtr($pluralforms, array(" " => "", "\t" => ""));
// Select the parts that define nplurals and plural.
$nplurals = strstr($pluralforms, "nplurals=");
if (strpos($nplurals, ";")) {
// We want the string from the 10th char, because "nplurals=" length is 9.
$nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9);
}
else {
return FALSE;
}
$plural = strstr($pluralforms, "plural=");
if (strpos($plural, ";")) {
// We want the string from the 8th char, because "plural=" length is 7.
$plural = substr($plural, 7, strpos($plural, ";") - 7);
}
else {
return FALSE;
}
// If the number of plurals is zero, we return a default result.
if ($nplurals == 0) {
return array($nplurals, array('default' => 0));
}
// Calculate possible plural positions of different plural values. All known
// plural formula's are repetitive above 100.
// For data compression we store the last position the array value
// changes and store it as default.
$element_stack = $this->parseArithmetic($plural);
if ($element_stack !== FALSE) {
for ($i = 0; $i <= 199; $i++) {
$plurals[$i] = $this->evaluatePlural($element_stack, $i);
}
$default = $plurals[$i - 1];
$plurals = array_filter($plurals, function ($value) use ($default) {
return ($value != $default);
});
$plurals['default'] = $default;
return array($nplurals, $plurals);
}
else {
throw new \Exception('The plural formula could not be parsed.');
}
}
/**
* Parses a Gettext Portable Object file header.
*
* @param string $header
* A string containing the complete header.
*
* @return array
* An associative array of key-value pairs.
*/
private function parseHeader($header) {
$header_parsed = array();
$lines = array_map('trim', explode("\n", $header));
foreach ($lines as $line) {
if ($line) {
list($tag, $contents) = explode(":", $line, 2);
$header_parsed[trim($tag)] = trim($contents);
}
}
return $header_parsed;
}
/**
* Parses and sanitizes an arithmetic formula into a plural element stack.
*
* While parsing, we ensure, that the operators have the right
* precedence and associativity.
*
* @param string $string
* A string containing the arithmetic formula.
*
* @return
* A stack of values and operations to be evaluated.
*/
private function parseArithmetic($string) {
// Operator precedence table.
$precedence = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8);
// Right associativity.
$right_associativity = array("?" => 1, ":" => 1);
$tokens = $this->tokenizeFormula($string);
// Parse by converting into infix notation then back into postfix
// Operator stack - holds math operators and symbols.
$operator_stack = array();
// Element Stack - holds data to be operated on.
$element_stack = array();
foreach ($tokens as $token) {
$current_token = $token;
// Numbers and the $n variable are simply pushed into $element_stack.
if (is_numeric($token)) {
$element_stack[] = $current_token;
}
elseif ($current_token == "n") {
$element_stack[] = '$n';
}
elseif ($current_token == "(") {
$operator_stack[] = $current_token;
}
elseif ($current_token == ")") {
$topop = array_pop($operator_stack);
while (isset($topop) && ($topop != "(")) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
}
}
elseif (!empty($precedence[$current_token])) {
// If it's an operator, then pop from $operator_stack into
// $element_stack until the precedence in $operator_stack is less
// than current, then push into $operator_stack.
$topop = array_pop($operator_stack);
while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
}
if ($topop) {
// Return element to top.
$operator_stack[] = $topop;
}
// Parentheses are not needed.
$operator_stack[] = $current_token;
}
else {
return FALSE;
}
}
// Flush operator stack.
$topop = array_pop($operator_stack);
while ($topop != NULL) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
}
$return = $element_stack;
// Now validate stack.
$previous_size = count($element_stack) + 1;
while (count($element_stack) < $previous_size) {
$previous_size = count($element_stack);
for ($i = 2; $i < count($element_stack); $i++) {
$op = $element_stack[$i];
if (!empty($precedence[$op])) {
if ($op == ":") {
$f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")";
}
elseif ($op == "?") {
$f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1];
}
else {
$f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")";
}
array_splice($element_stack, $i - 2, 3, $f);
break;
}
}
}
// If only one element is left, the number of operators is appropriate.
return count($element_stack) == 1 ? $return : FALSE;
}
/**
* Tokenize the formula.
*
* @param string $formula
* A string containing the arithmetic formula.
*
* @return array
* List of arithmetic tokens identified in the formula.
*/
private function tokenizeFormula($formula) {
$formula = str_replace(" ", "", $formula);
$tokens = array();
for ($i = 0; $i < strlen($formula); $i++) {
if (is_numeric($formula[$i])) {
$num = $formula[$i];
$j = $i + 1;
while ($j < strlen($formula) && is_numeric($formula[$j])) {
$num .= $formula[$j];
$j++;
}
$i = $j - 1;
$tokens[] = $num;
}
elseif ($pos = strpos(" =<>!&|", $formula[$i])) {
$next = $formula[$i + 1];
switch ($pos) {
case 1:
case 2:
case 3:
case 4:
if ($next == '=') {
$tokens[] = $formula[$i] . '=';
$i++;
}
else {
$tokens[] = $formula[$i];
}
break;
case 5:
if ($next == '&') {
$tokens[] = '&&';
$i++;
}
else {
$tokens[] = $formula[$i];
}
break;
case 6:
if ($next == '|') {
$tokens[] = '||';
$i++;
}
else {
$tokens[] = $formula[$i];
}
break;
}
}
else {
$tokens[] = $formula[$i];
}
}
return $tokens;
}
/**
* Evaluate the plural element stack using a plural value.
*
* Using an element stack, which represents a plural formula, we calculate
* which plural string should be used for a given plural value.
*
* An example of plural formula parting and evaluation:
* Plural formula: 'n!=1'
* This formula is parsed by parseArithmetic() to a stack (array) of elements:
* array(
* 0 => '$n',
* 1 => '1',
* 2 => '!=',
* );
* The evaluatePlural() method evaluates the $element_stack using the plural
* value $n. Before the actual evaluation, the '$n' in the array is replaced
* by the value of $n.
* For example: $n = 2 results in:
* array(
* 0 => '2',
* 1 => '1',
* 2 => '!=',
* );
* The stack is processed until only one element is (the result) is left. In
* every iteration the top elements of the stack, up until the first operator,
* are evaluated. After evaluation the arguments and the operator itself are
* removed and replaced by the evaluation result. This is typically 2
* arguments and 1 element for the operator.
* Because the operator is '!=' the example stack is evaluated as:
* $f = (int) 2 != 1;
* The resulting stack is:
* array(
* 0 => 1,
* );
* With only one element left in the stack (the final result) the loop is
* terminated and the result is returned.
*
* @param array $element_stack
* Array of plural formula values and operators create by parseArithmetic().
* @param integer $n
* The @count number for which we are determining the right plural position.
*
* @return integer
* Number of the plural string to be used for the given plural value.
*
* @see parseArithmetic()
* @throws Exception
*/
protected function evaluatePlural($element_stack, $n) {
$count = count($element_stack);
$limit = $count;
// Replace the '$n' value in the formula by the plural value.
for ($i = 0; $i < $count; $i++) {
if ($element_stack[$i] === '$n') {
$element_stack[$i] = $n;
}
}
// We process the stack until only one element is (the result) is left.
// We limit the number of evaluation cycles to prevent an endless loop in
// case the stack contains an error.
while (isset($element_stack[1])) {
for ($i = 2; $i < $count; $i++) {
// There's no point in checking non-symbols. Also, switch(TRUE) would
// match any case and so it would break.
if (is_bool($element_stack[$i]) || is_numeric($element_stack[$i])) {
continue;
}
$f = NULL;
$length = 3;
$delta = 2;
switch ($element_stack[$i]) {
case '==':
$f = $element_stack[$i - 2] == $element_stack[$i - 1];
break;
case '!=':
$f = $element_stack[$i - 2] != $element_stack[$i - 1];
break;
case '<=':
$f = $element_stack[$i - 2] <= $element_stack[$i - 1];
break;
case '>=':
$f = $element_stack[$i - 2] >= $element_stack[$i - 1];
break;
case '<':
$f = $element_stack[$i - 2] < $element_stack[$i - 1];
break;
case '>':
$f = $element_stack[$i - 2] > $element_stack[$i - 1];
break;
case '+':
$f = $element_stack[$i - 2] + $element_stack[$i - 1];
break;
case '-':
$f = $element_stack[$i - 2] - $element_stack[$i - 1];
break;
case '*':
$f = $element_stack[$i - 2] * $element_stack[$i - 1];
break;
case '/':
$f = $element_stack[$i - 2] / $element_stack[$i - 1];
break;
case '%':
$f = $element_stack[$i - 2] % $element_stack[$i - 1];
break;
case '&&':
$f = $element_stack[$i - 2] && $element_stack[$i - 1];
break;
case '||':
$f = $element_stack[$i - 2] || $element_stack[$i - 1];
break;
case ':':
$f = $element_stack[$i - 3] ? $element_stack[$i - 2] : $element_stack[$i - 1];
// This operator has 3 preceding elements, instead of the default 2.
$length = 5;
$delta = 3;
break;
}
// If the element is an operator we remove the processed elements and
// store the result.
if (isset($f)) {
array_splice($element_stack, $i - $delta, $length, $f);
break;
}
}
}
if (!$limit) {
throw new \Exception('The plural formula could not be evaluated.');
}
return (int) $element_stack[0];
}
}

View file

@ -0,0 +1,287 @@
<?php
/**
* @file
* Contains \Drupal\Component\Gettext\PoItem.
*/
namespace Drupal\Component\Gettext;
/**
* PoItem handles one translation.
*
* @todo: This class contains some really old legacy code.
* @see https://www.drupal.org/node/1637662
*/
class PoItem {
/**
* The language code this translation is in.
*
* @car string
*/
private $_langcode;
/**
* The context this translation belongs to.
*
* @var string
*/
private $_context = '';
/**
* The source string or array of strings if it has plurals.
*
* @var string or array
* @see $_plural
*/
private $_source;
/**
* Flag indicating if this translation has plurals.
*
* @var bool
*/
private $_plural;
/**
* The comment of this translation.
*
* @var string
*/
private $_comment;
/**
* The translation string or array of strings if it has plurals.
*
* @var string or array
* @see $_plural
*/
private $_translation;
/**
* Gets the language code of the currently used language.
*
* @return string with langcode
*/
function getLangcode() {
return $this->_langcode;
}
/**
* Set the language code of the current language.
*
* @param string $langcode
*/
function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Gets the context this translation belongs to.
*
* @return string $context
*/
function getContext() {
return $this->_context;
}
/**
* Set the context this translation belongs to.
*
* @param string $context
*/
function setContext($context) {
$this->_context = $context;
}
/**
* Gets the source string or the array of strings if the translation has
* plurals.
*
* @return string or array $translation
*/
function getSource() {
return $this->_source;
}
/**
* Set the source string or the array of strings if the translation has
* plurals.
*
* @param string or array $source
*/
function setSource($source) {
$this->_source = $source;
}
/**
* Gets the translation string or the array of strings if the translation has
* plurals.
*
* @return string or array $translation
*/
function getTranslation() {
return $this->_translation;
}
/**
* Set the translation string or the array of strings if the translation has
* plurals.
*
* @param string or array $translation
*/
function setTranslation($translation) {
$this->_translation = $translation;
}
/**
* Set if the translation has plural values.
*
* @param bool $plural
*/
function setPlural($plural) {
$this->_plural = $plural;
}
/**
* Get if the translation has plural values.
*
* @return boolean $plural
*/
function isPlural() {
return $this->_plural;
}
/**
* Gets the comment of this translation.
*
* @return String $comment
*/
function getComment() {
return $this->_comment;
}
/**
* Set the comment of this translation.
*
* @param String $comment
*/
function setComment($comment) {
$this->_comment = $comment;
}
/**
* Create the PoItem from a structured array.
*
* @param array values
*/
public function setFromArray(array $values = array()) {
if (isset($values['context'])) {
$this->setContext($values['context']);
}
if (isset($values['source'])) {
$this->setSource($values['source']);
}
if (isset($values['translation'])) {
$this->setTranslation($values['translation']);
}
if (isset($values['comment'])){
$this->setComment($values['comment']);
}
if (isset($this->_source) &&
strpos($this->_source, LOCALE_PLURAL_DELIMITER) !== FALSE) {
$this->setSource(explode(LOCALE_PLURAL_DELIMITER, $this->_source));
$this->setTranslation(explode(LOCALE_PLURAL_DELIMITER, $this->_translation));
$this->setPlural(count($this->_translation) > 1);
}
}
/**
* Output the PoItem as a string.
*/
public function __toString() {
return $this->formatItem();
}
/**
* Format the POItem as a string.
*/
private function formatItem() {
$output = '';
// Format string context.
if (!empty($this->_context)) {
$output .= 'msgctxt ' . $this->formatString($this->_context);
}
// Format translation.
if ($this->_plural) {
$output .= $this->formatPlural();
}
else {
$output .= $this->formatSingular();
}
// Add one empty line to separate the translations.
$output .= "\n";
return $output;
}
/**
* Formats a plural translation.
*/
private function formatPlural() {
$output = '';
// Format source strings.
$output .= 'msgid ' . $this->formatString($this->_source[0]);
$output .= 'msgid_plural ' . $this->formatString($this->_source[1]);
foreach ($this->_translation as $i => $trans) {
if (isset($this->_translation[$i])) {
$output .= 'msgstr[' . $i . '] ' . $this->formatString($trans);
}
else {
$output .= 'msgstr[' . $i . '] ""' . "\n";
}
}
return $output;
}
/**
* Formats a singular translation.
*/
private function formatSingular() {
$output = '';
$output .= 'msgid ' . $this->formatString($this->_source);
$output .= 'msgstr ' . (isset($this->_translation) ? $this->formatString($this->_translation) : '""');
return $output;
}
/**
* Formats a string for output on multiple lines.
*/
private function formatString($string) {
// Escape characters for processing.
$string = addcslashes($string, "\0..\37\\\"");
// Always include a line break after the explicit \n line breaks from
// the source string. Otherwise wrap at 70 chars to accommodate the extra
// format overhead too.
$parts = explode("\n", wordwrap(str_replace('\n', "\\n\n", $string), 70, " \n"));
// Multiline string should be exported starting with a "" and newline to
// have all lines aligned on the same column.
if (count($parts) > 1) {
return "\"\"\n\"" . implode("\"\n\"", $parts) . "\"\n";
}
// Single line strings are output on the same line.
else {
return "\"$parts[0]\"\n";
}
}
}

View file

@ -0,0 +1,96 @@
<?php
/**
* @file
* Contains \Drupal\Component\Gettext\PoMemoryWriter.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoWriterInterface;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
/**
* Defines a Gettext PO memory writer, to be used by the installer.
*/
class PoMemoryWriter implements PoWriterInterface {
/**
* Array to hold all PoItem elements.
*
* @var array
*/
private $_items;
/**
* Constructor, initialize empty items.
*/
function __construct() {
$this->_items = array();
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItem().
*/
public function writeItem(PoItem $item) {
if (is_array($item->getSource())) {
$item->setSource(implode(LOCALE_PLURAL_DELIMITER, $item->getSource()));
$item->setTranslation(implode(LOCALE_PLURAL_DELIMITER, $item->getTranslation()));
}
$context = $item->getContext();
$this->_items[$context != NULL ? $context : ''][$item->getSource()] = $item->getTranslation();
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItems().
*/
public function writeItems(PoReaderInterface $reader, $count = -1) {
$forever = $count == -1;
while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
$this->writeItem($item);
}
}
/**
* Get all stored PoItem's.
*
* @return array PoItem
*/
public function getData() {
return $this->_items;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:setLangcode().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function setLangcode($langcode) {
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:getLangcode().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function getLangcode() {
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:getHeader().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function getHeader() {
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:setHeader().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function setHeader(PoHeader $header) {
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* @file
* Contains \Drupal\Component\Gettext\PoMetadataInterface.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoHeader;
/**
* Methods required for both reader and writer implementations.
*
* @see \Drupal\Component\Gettext\PoReaderInterface
* @see \Drupal\Component\Gettext\PoWriterInterface
*/
interface PoMetadataInterface {
/**
* Set language code.
*
* @param string $langcode
* Language code string.
*/
public function setLangcode($langcode);
/**
* Get language code.
*
* @return string
* Language code string.
*/
public function getLangcode();
/**
* Set header metadata.
*
* @param \Drupal\Component\Gettext\PoHeader $header
* Header object representing metadata in a PO header.
*/
public function setHeader(PoHeader $header);
/**
* Get header metadata.
*
* @return \Drupal\Component\Gettext\PoHeader $header
* Header instance representing metadata in a PO header.
*/
public function getHeader();
}

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\Component\Gettext\PoReaderInterface.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoMetadataInterface;
/**
* Shared interface definition for all Gettext PO Readers.
*/
interface PoReaderInterface extends PoMetadataInterface {
/**
* Reads and returns a PoItem (source/translation pair).
*
* @return \Drupal\Component\Gettext\PoItem
* Wrapper for item data instance.
*/
public function readItem();
}

View file

@ -0,0 +1,44 @@
<?php
/**
* @file
* Contains \Drupal\Component\Gettext\PoStreamInterface.
*/
namespace Drupal\Component\Gettext;
/**
* Common functions for file/stream based PO readers/writers.
*
* @see PoReaderInterface
* @see PoWriterInterface
*/
interface PoStreamInterface {
/**
* Open the stream. Set the URI for the stream earlier with setURI().
*/
public function open();
/**
* Close the stream.
*/
public function close();
/**
* Gets the URI of the PO stream that is being read or written.
*
* @return
* URI string for this stream.
*/
public function getURI();
/**
* Set the URI of the PO stream that is going to be read or written.
*
* @param $uri
* URI string to set for this stream.
*/
public function setURI($uri);
}

View file

@ -0,0 +1,602 @@
<?php
/**
* @file
* Contains \Drupal\Component\Gettext\PoStreamReader.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoReaderInterface;
use Drupal\Component\Gettext\PoStreamInterface;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Utility\SafeMarkup;
/**
* Implements Gettext PO stream reader.
*
* The PO file format parsing is implemented according to the documentation at
* http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
*/
class PoStreamReader implements PoStreamInterface, PoReaderInterface {
/**
* Source line number of the stream being parsed.
*
* @var int
*/
private $_line_number = 0;
/**
* Parser context for the stream reader state machine.
*
* Possible contexts are:
* - 'COMMENT' (#)
* - 'MSGID' (msgid)
* - 'MSGID_PLURAL' (msgid_plural)
* - 'MSGCTXT' (msgctxt)
* - 'MSGSTR' (msgstr or msgstr[])
* - 'MSGSTR_ARR' (msgstr_arg)
*
* @var string
*/
private $_context = 'COMMENT';
/**
* Current entry being read. Incomplete.
*
* @var array
*/
private $_current_item = array();
/**
* Current plural index for plural translations.
*
* @var int
*/
private $_current_plural_index = 0;
/**
* URI of the PO stream that is being read.
*
* @var string
*/
private $_uri = '';
/**
* Language code for the PO stream being read.
*
* @var string
*/
private $_langcode = NULL;
/**
* File handle of the current PO stream.
*
* @var resource
*/
private $_fd;
/**
* The PO stream header.
*
* @var \Drupal\Component\Gettext\PoHeader
*/
private $_header;
/**
* Object wrapper for the last read source/translation pair.
*
* @var \Drupal\Component\Gettext\PoItem
*/
private $_last_item;
/**
* Indicator of whether the stream reading is finished.
*
* @var bool
*/
private $_finished;
/**
* Array of translated error strings recorded on reading this stream so far.
*
* @var array
*/
private $_errors;
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getLangcode().
*/
public function getLangcode() {
return $this->_langcode;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setLangcode().
*/
public function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getHeader().
*/
public function getHeader() {
return $this->_header;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader().
*
* Not applicable to stream reading and therefore not implemented.
*/
public function setHeader(PoHeader $header) {
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::getURI().
*/
public function getURI() {
return $this->_uri;
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::setURI().
*/
public function setURI($uri) {
$this->_uri = $uri;
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::open().
*
* Opens the stream and reads the header. The stream is ready for reading
* items after.
*
* @throws Exception
* If the URI is not yet set.
*/
public function open() {
if (!empty($this->_uri)) {
$this->_fd = fopen($this->_uri, 'rb');
$this->readHeader();
}
else {
throw new \Exception('Cannot open stream without URI set.');
}
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::close().
*
* @throws Exception
* If the stream is not open.
*/
public function close() {
if ($this->_fd) {
fclose($this->_fd);
}
else {
throw new \Exception('Cannot close stream that is not open.');
}
}
/**
* Implements Drupal\Component\Gettext\PoReaderInterface::readItem().
*/
public function readItem() {
// Clear out the last item.
$this->_last_item = NULL;
// Read until finished with the stream or a complete item was identified.
while (!$this->_finished && is_null($this->_last_item)) {
$this->readLine();
}
return $this->_last_item;
}
/**
* Sets the seek position for the current PO stream.
*
* @param int $seek
* The new seek position to set.
*/
public function setSeek($seek) {
fseek($this->_fd, $seek);
}
/**
* Gets the pointer position of the current PO stream.
*/
public function getSeek() {
return ftell($this->_fd);
}
/**
* Read the header from the PO stream.
*
* The header is a special case PoItem, using the empty string as source and
* key-value pairs as translation. We just reuse the item reader logic to
* read the header.
*/
private function readHeader() {
$item = $this->readItem();
// Handle the case properly when the .po file is empty (0 bytes).
if (!$item) {
return;
}
$header = new PoHeader;
$header->setFromString(trim($item->getTranslation()));
$this->_header = $header;
}
/**
* Reads a line from the PO stream and stores data internally.
*
* Expands $this->_current_item based on new data for the current item. If
* this line ends the current item, it is saved with setItemFromArray() with
* data from $this->_current_item.
*
* An internal state machine is maintained in this reader using
* $this->_context as the reading state. PO items are in between COMMENT
* states (when items have at least one line or comment in between them) or
* indicated by MSGSTR or MSGSTR_ARR followed immediately by an MSGID or
* MSGCTXT (when items closely follow each other).
*
* @return
* FALSE if an error was logged, NULL otherwise. The errors are considered
* non-blocking, so reading can continue, while the errors are collected
* for later presentation.
*/
private function readLine() {
// Read a line and set the stream finished indicator if it was not
// possible anymore.
$line = fgets($this->_fd);
$this->_finished = ($line === FALSE);
if (!$this->_finished) {
if ($this->_line_number == 0) {
// The first line might come with a UTF-8 BOM, which should be removed.
$line = str_replace("\xEF\xBB\xBF", '', $line);
// Current plurality for 'msgstr[]'.
$this->_current_plural_index = 0;
}
// Track the line number for error reporting.
$this->_line_number++;
// Initialize common values for error logging.
$log_vars = array(
'%uri' => $this->getURI(),
'%line' => $this->_line_number,
);
// Trim away the linefeed. \\n might appear at the end of the string if
// another line continuing the same string follows. We can remove that.
$line = trim(strtr($line, array("\\\n" => "")));
if (!strncmp('#', $line, 1)) {
// Lines starting with '#' are comments.
if ($this->_context == 'COMMENT') {
// Already in comment context, add to current comment.
$this->_current_item['#'][] = substr($line, 1);
}
elseif (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
// We are currently in string context, save current item.
$this->setItemFromArray($this->_current_item);
// Start a new entry for the comment.
$this->_current_item = array();
$this->_current_item['#'][] = substr($line, 1);
$this->_context = 'COMMENT';
return;
}
else {
// A comment following any other context is a syntax error.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgstr" was expected but not found on line %line.', $log_vars);
return FALSE;
}
return;
}
elseif (!strncmp('msgid_plural', $line, 12)) {
// A plural form for the current source string.
if ($this->_context != 'MSGID') {
// A plural form can only be added to an msgid directly.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgid_plural" was expected but not found on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgid_plural' and trim away whitespace.
$line = trim(substr($line, 12));
// Only the plural source string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The plural form must be wrapped in quotes.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains a syntax error on line %line.', $log_vars);
return FALSE;
}
// Append the plural source to the current entry.
if (is_string($this->_current_item['msgid'])) {
// The first value was stored as string. Now we know the context is
// plural, it is converted to array.
$this->_current_item['msgid'] = array($this->_current_item['msgid']);
}
$this->_current_item['msgid'][] = $quoted;
$this->_context = 'MSGID_PLURAL';
return;
}
elseif (!strncmp('msgid', $line, 5)) {
// Starting a new message.
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
// We are currently in string context, save current item.
$this->setItemFromArray($this->_current_item);
// Start a new context for the msgid.
$this->_current_item = array();
}
elseif ($this->_context == 'MSGID') {
// We are currently already in the context, meaning we passed an id with no data.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgid" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgid' and trim away whitespace.
$line = trim(substr($line, 5));
// Only the message id string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The message id must be wrapped in quotes.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: invalid format for "msgid" on line %line.', $log_vars, $log_vars);
return FALSE;
}
$this->_current_item['msgid'] = $quoted;
$this->_context = 'MSGID';
return;
}
elseif (!strncmp('msgctxt', $line, 7)) {
// Starting a new context.
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
// We are currently in string context, save current item.
$this->setItemFromArray($this->_current_item);
$this->_current_item = array();
}
elseif (!empty($this->_current_item['msgctxt'])) {
// A context cannot apply to another context.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgctxt" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgctxt' and trim away whitespaces.
$line = trim(substr($line, 7));
// Only the msgctxt string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The context string must be quoted.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: invalid format for "msgctxt" on line %line.', $log_vars);
return FALSE;
}
$this->_current_item['msgctxt'] = $quoted;
$this->_context = 'MSGCTXT';
return;
}
elseif (!strncmp('msgstr[', $line, 7)) {
// A message string for a specific plurality.
if (($this->_context != 'MSGID') &&
($this->_context != 'MSGCTXT') &&
($this->_context != 'MSGID_PLURAL') &&
($this->_context != 'MSGSTR_ARR')) {
// Plural message strings must come after msgid, msgxtxt,
// msgid_plural, or other msgstr[] entries.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgstr[]" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Ensure the plurality is terminated.
if (strpos($line, ']') === FALSE) {
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
return FALSE;
}
// Extract the plurality.
$frombracket = strstr($line, '[');
$this->_current_plural_index = substr($frombracket, 1, strpos($frombracket, ']') - 1);
// Skip to the next whitespace and trim away any further whitespace,
// bringing $line to the message text only.
$line = trim(strstr($line, " "));
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
return FALSE;
}
if (!isset($this->_current_item['msgstr']) || !is_array($this->_current_item['msgstr'])) {
$this->_current_item['msgstr'] = array();
}
$this->_current_item['msgstr'][$this->_current_plural_index] = $quoted;
$this->_context = 'MSGSTR_ARR';
return;
}
elseif (!strncmp("msgstr", $line, 6)) {
// A string pair for an msgid (with optional context).
if (($this->_context != 'MSGID') && ($this->_context != 'MSGCTXT')) {
// Strings are only valid within an id or context scope.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: "msgstr" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgstr' and trim away away whitespaces.
$line = trim(substr($line, 6));
// Only the msgstr string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: invalid format for "msgstr" on line %line.', $log_vars);
return FALSE;
}
$this->_current_item['msgstr'] = $quoted;
$this->_context = 'MSGSTR';
return;
}
elseif ($line != '') {
// Anything that is not a token may be a continuation of a previous token.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// This string must be quoted.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: string continuation expected on line %line.', $log_vars);
return FALSE;
}
// Append the string to the current item.
if (($this->_context == 'MSGID') || ($this->_context == 'MSGID_PLURAL')) {
if (is_array($this->_current_item['msgid'])) {
// Add string to last array element for plural sources.
$last_index = count($this->_current_item['msgid']) - 1;
$this->_current_item['msgid'][$last_index] .= $quoted;
}
else {
// Singular source, just append the string.
$this->_current_item['msgid'] .= $quoted;
}
}
elseif ($this->_context == 'MSGCTXT') {
// Multiline context name.
$this->_current_item['msgctxt'] .= $quoted;
}
elseif ($this->_context == 'MSGSTR') {
// Multiline translation string.
$this->_current_item['msgstr'] .= $quoted;
}
elseif ($this->_context == 'MSGSTR_ARR') {
// Multiline plural translation string.
$this->_current_item['msgstr'][$this->_current_plural_index] .= $quoted;
}
else {
// No valid context to append to.
$this->_errors[] = SafeMarkup::format('The translation stream %uri contains an error: unexpected string on line %line.', $log_vars);
return FALSE;
}
return;
}
}
// Empty line read or EOF of PO stream, close out the last entry.
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
$this->setItemFromArray($this->_current_item);
$this->_current_item = array();
}
elseif ($this->_context != 'COMMENT') {
$this->_errors[] = SafeMarkup::format('The translation stream %uri ended unexpectedly at line %line.', $log_vars);
return FALSE;
}
}
/**
* Store the parsed values as a PoItem object.
*/
public function setItemFromArray($value) {
$plural = FALSE;
$comments = '';
if (isset($value['#'])) {
$comments = $this->shortenComments($value['#']);
}
if (is_array($value['msgstr'])) {
// Sort plural variants by their form index.
ksort($value['msgstr']);
$plural = TRUE;
}
$item = new PoItem();
$item->setContext(isset($value['msgctxt']) ? $value['msgctxt'] : '');
$item->setSource($value['msgid']);
$item->setTranslation($value['msgstr']);
$item->setPlural($plural);
$item->setComment($comments);
$item->setLangcode($this->_langcode);
$this->_last_item = $item;
$this->_context = 'COMMENT';
}
/**
* Parses a string in quotes.
*
* @param $string
* A string specified with enclosing quotes.
*
* @return
* The string parsed from inside the quotes.
*/
function parseQuoted($string) {
if (substr($string, 0, 1) != substr($string, -1, 1)) {
// Start and end quotes must be the same.
return FALSE;
}
$quote = substr($string, 0, 1);
$string = substr($string, 1, -1);
if ($quote == '"') {
// Double quotes: strip slashes.
return stripcslashes($string);
}
elseif ($quote == "'") {
// Simple quote: return as-is.
return $string;
}
else {
// Unrecognized quote.
return FALSE;
}
}
/**
* Generates a short, one-string version of the passed comment array.
*
* @param $comment
* An array of strings containing a comment.
*
* @return
* Short one-string version of the comment.
*/
private function shortenComments($comment) {
$comm = '';
while (count($comment)) {
$test = $comm . substr(array_shift($comment), 1) . ', ';
if (strlen($comm) < 130) {
$comm = $test;
}
else {
break;
}
}
return trim(substr($comm, 0, -2));
}
}

View file

@ -0,0 +1,168 @@
<?php
/**
* @file
* Contains \Drupal\Component\Gettext\PoStreamWriter.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
use Drupal\Component\Gettext\PoReaderInterface;
use Drupal\Component\Gettext\PoWriterInterface;
use Drupal\Component\Gettext\PoStreamInterface;
/**
* Defines a Gettext PO stream writer.
*/
class PoStreamWriter implements PoWriterInterface, PoStreamInterface {
/**
* URI of the PO stream that is being written.
*
* @var string
*/
private $_uri;
/**
* The Gettext PO header.
*
* @var \Drupal\Component\Gettext\PoHeader
*/
private $_header;
/**
* File handle of the current PO stream.
*
* @var resource
*/
private $_fd;
/**
* Gets the PO header of the current stream.
*
* @return \Drupal\Component\Gettext\PoHeader
* The Gettext PO header.
*/
public function getHeader() {
return $this->_header;
}
/**
* Set the PO header for the current stream.
*
* @param \Drupal\Component\Gettext\PoHeader $header
* The Gettext PO header to set.
*/
public function setHeader(PoHeader $header) {
$this->_header = $header;
}
/**
* Gets the current language code used.
*
* @return string
* The language code.
*/
public function getLangcode() {
return $this->_langcode;
}
/**
* Set the language code.
*
* @param string $langcode
* The language code.
*/
public function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::open().
*/
public function open() {
// Open in write mode. Will overwrite the stream if it already exists.
$this->_fd = fopen($this->getURI(), 'w');
// Write the header at the start.
$this->writeHeader();
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::close().
*
* @throws Exception
* If the stream is not open.
*/
public function close() {
if ($this->_fd) {
fclose($this->_fd);
}
else {
throw new Exception('Cannot close stream that is not open.');
}
}
/**
* Write data to the stream.
*
* @param string $data
* Piece of string to write to the stream. If the value is not directly a
* string, casting will happen in writing.
*
* @throws Exception
* If writing the data is not possible.
*/
private function write($data) {
$result = fputs($this->_fd, $data);
if ($result === FALSE) {
throw new Exception('Unable to write data: ' . substr($data, 0, 20));
}
}
/**
* Write the PO header to the stream.
*/
private function writeHeader() {
$this->write($this->_header);
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItem().
*/
public function writeItem(PoItem $item) {
$this->write($item);
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItems().
*/
public function writeItems(PoReaderInterface $reader, $count = -1) {
$forever = $count == -1;
while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
$this->writeItem($item);
}
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::getURI().
*
* @throws Exception
* If the URI is not set.
*/
public function getURI() {
if (empty($this->_uri)) {
throw new Exception('No URI set.');
}
return $this->_uri;
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::setURI().
*/
public function setURI($uri) {
$this->_uri = $uri;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\Component\Gettext\PoWriterInterface.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoMetadataInterface;
use Drupal\Component\Gettext\PoItem;
/**
* Shared interface definition for all Gettext PO Writers.
*/
interface PoWriterInterface extends PoMetadataInterface {
/**
* Writes the given item.
*
* @param PoItem $item
* One specific item to write.
*/
public function writeItem(PoItem $item);
/**
* Writes all or the given amount of items.
*
* @param PoReaderInterface $reader
* Reader to read PoItems from.
* @param $count
* Amount of items to read from $reader to write. If -1, all items are
* read from $reader.
*/
public function writeItems(PoReaderInterface $reader, $count = -1);
}

View file

@ -0,0 +1,162 @@
<?php
/**
* @file
* Contains \Drupal\Component\Graph\Graph.
*/
namespace Drupal\Component\Graph;
/**
* Directed acyclic graph manipulation.
*/
class Graph {
/**
* Holds the directed acyclic graph.
*/
protected $graph;
/**
* Instantiates the depth first search object.
*
* @param $graph
* A three dimensional associated array, with the first keys being the names
* of the vertices, these can be strings or numbers. The second key is
* 'edges' and the third one are again vertices, each such key representing
* an edge. Values of array elements are copied over.
*
* Example:
* @code
* $graph[1]['edges'][2] = 1;
* $graph[2]['edges'][3] = 1;
* $graph[2]['edges'][4] = 1;
* $graph[3]['edges'][4] = 1;
* @endcode
*
* On return you will also have:
* @code
* $graph[1]['paths'][2] = 1;
* $graph[1]['paths'][3] = 1;
* $graph[2]['reverse_paths'][1] = 1;
* $graph[3]['reverse_paths'][1] = 1;
* @endcode
*/
public function __construct($graph) {
$this->graph = $graph;
}
/**
* Performs a depth-first search and sort on the directed acyclic graph.
*
* @return
* The given $graph with more secondary keys filled in:
* - 'paths': Contains a list of vertices than can be reached on a path from
* this vertex.
* - 'reverse_paths': Contains a list of vertices that has a path from them
* to this vertex.
* - 'weight': If there is a path from a vertex to another then the weight of
* the latter is higher.
* - 'component': Vertices in the same component have the same component
* identifier.
*/
public function searchAndSort() {
$state = array(
// The order of last visit of the depth first search. This is the reverse
// of the topological order if the graph is acyclic.
'last_visit_order' => array(),
// The components of the graph.
'components' => array(),
);
// Perform the actual search.
foreach ($this->graph as $start => $data) {
$this->depthFirstSearch($state, $start);
}
// We do such a numbering that every component starts with 0. This is useful
// for module installs as we can install every 0 weighted module in one
// request, and then every 1 weighted etc.
$component_weights = array();
foreach ($state['last_visit_order'] as $vertex) {
$component = $this->graph[$vertex]['component'];
if (!isset($component_weights[$component])) {
$component_weights[$component] = 0;
}
$this->graph[$vertex]['weight'] = $component_weights[$component]--;
}
return $this->graph;
}
/**
* Performs a depth-first search on a graph.
*
* @param $state
* An associative array. The key 'last_visit_order' stores a list of the
* vertices visited. The key components stores list of vertices belonging
* to the same the component.
* @param $start
* An arbitrary vertex where we started traversing the graph.
* @param $component
* The component of the last vertex.
*
* @see \Drupal\Component\Graph\Graph::searchAndSort()
*/
protected function depthFirstSearch(&$state, $start, &$component = NULL) {
// Assign new component for each new vertex, i.e. when not called recursively.
if (!isset($component)) {
$component = $start;
}
// Nothing to do, if we already visited this vertex.
if (isset($this->graph[$start]['paths'])) {
return;
}
// Mark $start as visited.
$this->graph[$start]['paths'] = array();
// Assign $start to the current component.
$this->graph[$start]['component'] = $component;
$state['components'][$component][] = $start;
// Visit edges of $start.
if (isset($this->graph[$start]['edges'])) {
foreach ($this->graph[$start]['edges'] as $end => $v) {
// Mark that $start can reach $end.
$this->graph[$start]['paths'][$end] = $v;
if (isset($this->graph[$end]['component']) && $component != $this->graph[$end]['component']) {
// This vertex already has a component, use that from now on and
// reassign all the previously explored vertices.
$new_component = $this->graph[$end]['component'];
foreach ($state['components'][$component] as $vertex) {
$this->graph[$vertex]['component'] = $new_component;
$state['components'][$new_component][] = $vertex;
}
unset($state['components'][$component]);
$component = $new_component;
}
// Only visit existing vertices.
if (isset($this->graph[$end])) {
// Visit the connected vertex.
$this->depthFirstSearch($state, $end, $component);
// All vertices reachable by $end are also reachable by $start.
$this->graph[$start]['paths'] += $this->graph[$end]['paths'];
}
}
}
// Now that any other subgraph has been explored, add $start to all reverse
// paths.
foreach ($this->graph[$start]['paths'] as $end => $v) {
if (isset($this->graph[$end])) {
$this->graph[$end]['reverse_paths'][$start] = $v;
}
}
// Record the order of the last visit. This is the reverse of the
// topological order if the graph is acyclic.
$state['last_visit_order'][] = $start;
}
}

View file

@ -0,0 +1,105 @@
<?php
/**
* @file
* Contains \Drupal\Component\PhpStorage\FileReadOnlyStorage.
*/
namespace Drupal\Component\PhpStorage;
/**
* Reads code as regular PHP files, but won't write them.
*/
class FileReadOnlyStorage implements PhpStorageInterface {
/**
* The directory where the files should be stored.
*
* @var string
*/
protected $directory;
/**
* Constructs this FileStorage object.
*
* @param $configuration
* An associative array, containing at least two keys (the rest are ignored):
* - directory: The directory where the files should be stored.
* - bin: The storage bin. Multiple storage objects can be instantiated with
* the same configuration, but for different bins.
*/
public function __construct(array $configuration) {
$this->directory = $configuration['directory'] . '/' . $configuration['bin'];
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::exists().
*/
public function exists($name) {
return file_exists($this->getFullPath($name));
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::load().
*/
public function load($name) {
// The FALSE returned on failure is enough for the caller to handle this,
// we do not want a warning too.
return (@include_once $this->getFullPath($name)) !== FALSE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::save().
*/
public function save($name, $code) {
return FALSE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::delete().
*/
public function delete($name) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getFullPath($name) {
return $this->directory . '/' . $name;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::writeable().
*/
function writeable() {
return FALSE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::deleteAll().
*/
public function deleteAll() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function listAll() {
$names = array();
if (file_exists($this->directory)) {
foreach (new \DirectoryIterator($this->directory) as $fileinfo) {
if (!$fileinfo->isDot()) {
$name = $fileinfo->getFilename();
if ($name != '.htaccess') {
$names[] = $name;
}
}
}
}
return $names;
}
}

View file

@ -0,0 +1,269 @@
<?php
/**
* @file
* Contains \Drupal\Component\PhpStorage\FileStorage.
*/
namespace Drupal\Component\PhpStorage;
/**
* Stores the code as regular PHP files.
*/
class FileStorage implements PhpStorageInterface {
/**
* The directory where the files should be stored.
*
* @var string
*/
protected $directory;
/**
* Constructs this FileStorage object.
*
* @param array $configuration
* An associative array, containing at least these two keys:
* - directory: The directory where the files should be stored.
* - bin: The storage bin. Multiple storage objects can be instantiated with
* the same configuration, but for different bins..
*/
public function __construct(array $configuration) {
$this->directory = $configuration['directory'] . '/' . $configuration['bin'];
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::exists().
*/
public function exists($name) {
return file_exists($this->getFullPath($name));
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::load().
*/
public function load($name) {
// The FALSE returned on failure is enough for the caller to handle this,
// we do not want a warning too.
return (@include_once $this->getFullPath($name)) !== FALSE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::save().
*/
public function save($name, $code) {
$path = $this->getFullPath($name);
$directory = dirname($path);
if ($this->ensureDirectory($directory)) {
$htaccess_path = $directory . '/.htaccess';
if (!file_exists($htaccess_path) && file_put_contents($htaccess_path, static::htaccessLines())) {
@chmod($htaccess_path, 0444);
}
}
return (bool) file_put_contents($path, $code);
}
/**
* Returns the standard .htaccess lines that Drupal writes to file directories.
*
* @param bool $private
* (optional) Set to FALSE to return the .htaccess lines for an open and
* public directory. The default is TRUE, which returns the .htaccess lines
* for a private and protected directory.
*
* @return string
* The desired contents of the .htaccess file.
*
* @see file_create_htaccess()
*/
public static function htaccessLines($private = TRUE) {
$lines = <<<EOF
# Turn off all options we don't need.
Options None
Options +FollowSymLinks
# Set the catch-all handler to prevent scripts from being executed.
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
<Files *>
# Override the handler again if we're run later in the evaluation list.
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
</Files>
# If we know how to do it safely, disable the PHP engine entirely.
<IfModule mod_php5.c>
php_flag engine off
</IfModule>
EOF;
if ($private) {
$lines = <<<EOF
# Deny all requests from Apache 2.4+.
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
# Deny all requests from Apache 2.0-2.2.
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>
$lines
EOF;
}
return $lines;
}
/**
* Ensures the directory exists, has the right permissions, and a .htaccess.
*
* For compatibility with open_basedir, the requested directory is created
* using a recursion logic that is based on the relative directory path/tree:
* It works from the end of the path recursively back towards the root
* directory, until an existing parent directory is found. From there, the
* subdirectories are created.
*
* @param string $directory
* The directory path.
* @param int $mode
* The mode, permissions, the directory should have.
*
* @return bool
* TRUE if the directory exists or has been created, FALSE otherwise.
*/
protected function ensureDirectory($directory, $mode = 0777) {
if ($this->createDirectory($directory, $mode)) {
$htaccess_path = $directory . '/.htaccess';
if (!file_exists($htaccess_path) && file_put_contents($htaccess_path, static::htaccessLines())) {
@chmod($htaccess_path, 0444);
}
}
}
/**
* Ensures the requested directory exists and has the right permissions.
*
* For compatibility with open_basedir, the requested directory is created
* using a recursion logic that is based on the relative directory path/tree:
* It works from the end of the path recursively back towards the root
* directory, until an existing parent directory is found. From there, the
* subdirectories are created.
*
* @param string $directory
* The directory path.
* @param int $mode
* The mode, permissions, the directory should have.
* @param bool $is_backwards_recursive
* Internal use only.
*
* @return bool
* TRUE if the directory exists or has been created, FALSE otherwise.
*/
protected function createDirectory($directory, $mode = 0777, $is_backwards_recursive = FALSE) {
// If the directory exists already, there's nothing to do.
if (is_dir($directory)) {
return TRUE;
}
// Otherwise, try to create the directory and ensure to set its permissions,
// because mkdir() obeys the umask of the current process.
if (is_dir($parent = dirname($directory))) {
// If the parent directory exists, then the backwards recursion must end,
// regardless of whether the subdirectory could be created.
if ($status = mkdir($directory)) {
// Only try to chmod() if the subdirectory could be created.
$status = chmod($directory, $mode);
}
return $is_backwards_recursive ? TRUE : $status;
}
// If the parent directory and the requested directory does not exist and
// could not be created above, walk the requested directory path back up
// until an existing directory is hit, and from there, recursively create
// the sub-directories. Only if that recursion succeeds, create the final,
// originally requested subdirectory.
return $this->createDirectory($parent, $mode, TRUE) && mkdir($directory) && chmod($directory, $mode);
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::delete().
*/
public function delete($name) {
$path = $this->getFullPath($name);
if (file_exists($path)) {
return $this->unlink($path);
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getFullPath($name) {
return $this->directory . '/' . $name;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::writeable().
*/
public function writeable() {
return TRUE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::deleteAll().
*/
public function deleteAll() {
return $this->unlink($this->directory);
}
/**
* Deletes files and/or directories in the specified path.
*
* If the specified path is a directory the method will
* call itself recursively to process the contents. Once the contents have
* been removed the directory will also be removed.
*
* @param string $path
* A string containing either a file or directory path.
*
* @return boolean
* TRUE for success or if path does not exist, FALSE in the event of an
* error.
*/
protected function unlink($path) {
if (file_exists($path)) {
if (is_dir($path)) {
// Ensure the folder is writable.
@chmod($path, 0777);
foreach (new \DirectoryIterator($path) as $fileinfo) {
if (!$fileinfo->isDot()) {
$this->unlink($fileinfo->getPathName());
}
}
return @rmdir($path);
}
// Windows needs the file to be writable.
@chmod($path, 0700);
return @unlink($path);
}
// If there's nothing to delete return TRUE anyway.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function listAll() {
$names = array();
if (file_exists($this->directory)) {
foreach (new \DirectoryIterator($this->directory) as $fileinfo) {
if (!$fileinfo->isDot()) {
$name = $fileinfo->getFilename();
if ($name != '.htaccess') {
$names[] = $name;
}
}
}
}
return $names;
}
}

View file

@ -0,0 +1,211 @@
<?php
/**
* @file
* Contains \Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage.
*/
namespace Drupal\Component\PhpStorage;
/**
* Stores PHP code in files with securely hashed names.
*
* The goal of this class is to ensure that if a PHP file is replaced with
* an untrusted one, it does not get loaded. Since mtime granularity is 1
* second, we cannot prevent an attack that happens within one second of the
* initial save(). However, it is very unlikely for an attacker exploiting an
* upload or file write vulnerability to also know when a legitimate file is
* being saved, discover its hash, undo its file permissions, and override the
* file with an upload all within a single second. Being able to accomplish
* that would indicate a site very likely vulnerable to many other attack
* vectors.
*
* Each file is stored in its own unique containing directory. The hash is based
* on the virtual file name, the containing directory's mtime, and a
* cryptographically hard to guess secret string. Thus, even if the hashed file
* name is discovered and replaced by an untrusted file (e.g., via a
* move_uploaded_file() invocation by a script that performs insufficient
* validation), the directory's mtime gets updated in the process, invalidating
* the hash and preventing the untrusted file from getting loaded.
*
* This class does not protect against overwriting a file in-place (e.g. a
* malicious module that does a file_put_contents()) since this will not change
* the mtime of the directory. MTimeProtectedFileStorage protects against this
* at the cost of an additional system call for every load() and exists().
*
* The containing directory is created with the same name as the virtual file
* name (slashes removed) to assist with debugging, since the file itself is
* stored with a name that's meaningless to humans.
*/
class MTimeProtectedFastFileStorage extends FileStorage {
/**
* The secret used in the HMAC.
*
* @var string
*/
protected $secret;
/**
* Constructs this MTimeProtectedFastFileStorage object.
*
* @param array $configuration
* An associated array, containing at least these keys (the rest are
* ignored):
* - directory: The directory where the files should be stored.
* - secret: A cryptographically hard to guess secret string.
* -bin. The storage bin. Multiple storage objects can be instantiated with
* the same configuration, but for different bins.
*/
public function __construct(array $configuration) {
parent::__construct($configuration);
$this->secret = $configuration['secret'];
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::save().
*/
public function save($name, $data) {
$this->ensureDirectory($this->directory);
// Write the file out to a temporary location. Prepend with a '.' to keep it
// hidden from listings and web servers.
$temporary_path = $this->tempnam($this->directory, '.');
if (!$temporary_path || !@file_put_contents($temporary_path, $data)) {
return FALSE;
}
// The file will not be chmod() in the future so this is the final
// permission.
chmod($temporary_path, 0444);
// Prepare a directory dedicated for just this file. Ensure it has a current
// mtime so that when the file (hashed on that mtime) is moved into it, the
// mtime remains the same (unless the clock ticks to the next second during
// the rename, in which case we'll try again).
$directory = $this->getContainingDirectoryFullPath($name);
if (file_exists($directory)) {
$this->unlink($directory);
}
$this->ensureDirectory($directory);
// Move the file to its final place. The mtime of a directory is the time of
// the last file create or delete in the directory. So the moving will
// update the directory mtime. However, this update will very likely not
// show up, because it has a coarse, one second granularity and typical
// moves takes significantly less than that. In the unlucky case the clock
// ticks during the move, we need to keep trying until the mtime we hashed
// on and the updated mtime match.
$previous_mtime = 0;
$i = 0;
while (($mtime = $this->getUncachedMTime($directory)) && ($mtime != $previous_mtime)) {
$previous_mtime = $mtime;
// Reset the file back in the temporary location if this is not the first
// iteration.
if ($i > 0) {
$this->unlink($temporary_path);
$temporary_path = $this->tempnam($this->directory, '.');
rename($full_path, $temporary_path);
// Make sure to not loop infinitely on a hopelessly slow filesystem.
if ($i > 10) {
$this->unlink($temporary_path);
return FALSE;
}
}
$full_path = $this->getFullPath($name, $directory, $mtime);
rename($temporary_path, $full_path);
$i++;
}
return TRUE;
}
/**
* Gets the full path where the file is or should be stored.
*
* This function creates a file path that includes a unique containing
* directory for the file and a file name that is a hash of the virtual file
* name, a cryptographic secret, and the containing directory mtime. If the
* file is overridden by an insecure upload script, the directory mtime gets
* modified, invalidating the file, thus protecting against untrusted code
* getting executed.
*
* @param string $name
* The virtual file name. Can be a relative path.
* @param string $directory
* (optional) The directory containing the file. If not passed, this is
* retrieved by calling getContainingDirectoryFullPath().
* @param int $directory_mtime
* (optional) The mtime of $directory. Can be passed to avoid an extra
* filesystem call when the mtime of the directory is already known.
*
* @return string
* The full path where the file is or should be stored.
*/
public function getFullPath($name, &$directory = NULL, &$directory_mtime = NULL) {
if (!isset($directory)) {
$directory = $this->getContainingDirectoryFullPath($name);
}
if (!isset($directory_mtime)) {
$directory_mtime = file_exists($directory) ? filemtime($directory) : 0;
}
return $directory . '/' . hash_hmac('sha256', $name, $this->secret . $directory_mtime) . '.php';
}
/**
* {@inheritdoc}
*/
public function delete($name) {
$path = $this->getContainingDirectoryFullPath($name);
if (file_exists($path)) {
return $this->unlink($path);
}
return FALSE;
}
/**
* Gets the full path of the containing directory where the file is or should
* be stored.
*
* @param string $name
* The virtual file name. Can be a relative path.
*
* @return string
* The full path of the containing directory where the file is or should be
* stored.
*/
protected function getContainingDirectoryFullPath($name) {
// Remove the .php file extension from the directory name.
// Within a single directory, a subdirectory cannot have the same name as a
// file. Thus, when switching between MTimeProtectedFastFileStorage and
// FileStorage, the subdirectory or the file cannot be created in case the
// other file type exists already.
if (substr($name, -4) === '.php') {
$name = substr($name, 0, -4);
}
return $this->directory . '/' . str_replace('/', '#', $name);
}
/**
* Clears PHP's stat cache and returns the directory's mtime.
*/
protected function getUncachedMTime($directory) {
clearstatcache(TRUE, $directory);
return filemtime($directory);
}
/**
* A brute force tempnam implementation supporting streams.
*
* @param $directory
* The directory where the temporary filename will be created.
* @param $prefix
* The prefix of the generated temporary filename.
* @return string
* Returns the new temporary filename (with path), or FALSE on failure.
*/
protected function tempnam($directory, $prefix) {
do {
$path = $directory . '/' . $prefix . substr(str_shuffle(hash('sha256', microtime())), 0, 10);
} while (file_exists($path));
return $path;
}
}

View file

@ -0,0 +1,76 @@
<?php
/**
* @file
* Contains \Drupal\Component\PhpStorage\MTimeProtectedFileStorage.
*/
namespace Drupal\Component\PhpStorage;
/**
* Stores PHP code in files with securely hashed names.
*
* The goal of this class is to ensure that if a PHP file is replaced with
* an untrusted one, it does not get loaded. Since mtime granularity is 1
* second, we cannot prevent an attack that happens within one second of the
* initial save(). However, it is very unlikely for an attacker exploiting an
* upload or file write vulnerability to also know when a legitimate file is
* being saved, discover its hash, undo its file permissions, and override the
* file with an upload all within a single second. Being able to accomplish
* that would indicate a site very likely vulnerable to many other attack
* vectors.
*
* Each file is stored in its own unique containing directory. The hash is
* based on the virtual file name, the containing directory's mtime, and a
* cryptographically hard to guess secret string. Thus, even if the hashed file
* name is discovered and replaced by an untrusted file (e.g., via a
* move_uploaded_file() invocation by a script that performs insufficient
* validation), the directory's mtime gets updated in the process, invalidating
* the hash and preventing the untrusted file from getting loaded. Also, the
* file mtime will be checked providing security against overwriting in-place,
* at the cost of an additional system call for every load() and exists().
*
* The containing directory is created with the same name as the virtual file
* name (slashes replaced with hashmarks) to assist with debugging, since the
* file itself is stored with a name that's meaningless to humans.
*/
class MTimeProtectedFileStorage extends MTimeProtectedFastFileStorage {
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::load().
*/
public function load($name) {
if (($filename = $this->checkFile($name)) !== FALSE) {
// Inline parent::load() to avoid an expensive getFullPath() call.
return (@include_once $filename) !== FALSE;
}
return FALSE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::exists().
*/
public function exists($name) {
return $this->checkFile($name) !== FALSE;
}
/**
* Determines whether a protected file exists and sets the filename too.
*
* @param string $name
* The virtual file name. Can be a relative path.
* return string
* The full path where the file is if it is valid, FALSE otherwise.
*/
protected function checkFile($name) {
$filename = $this->getFullPath($name, $directory, $directory_mtime);
return file_exists($filename) && filemtime($filename) <= $directory_mtime ? $filename : FALSE;
}
/**
* {@inheritdoc}
*/
public function getPath($name) {
return $this->checkFile($name);
}
}

View file

@ -0,0 +1,102 @@
<?php
/**
* @file
* Contains \Drupal\Component\PhpStorage\PhpStorageInterface.
*/
namespace Drupal\Component\PhpStorage;
/**
* Stores and loads PHP code.
*
* Each interface function takes $name as a parameter. This is a virtual file
* name: for example, 'foo.php' or 'some/relative/path/to/foo.php'. The
* storage implementation may store these as files within the local file system,
* use a remote stream, combine multiple virtual files into an archive, store
* them in database records, or use some other storage technique.
*/
interface PhpStorageInterface {
/**
* Checks whether the PHP code exists in storage.
*
* @param string $name
* The virtual file name. Can be a relative path.
*
* @return bool
* TRUE if the virtual file exists, FALSE otherwise.
*/
public function exists($name);
/**
* Loads PHP code from storage.
*
* Depending on storage implementation, exists() checks can be expensive, so
* this function may be called for a file that doesn't exist, and that should
* not result in errors. This function does not return anything, so it is
* up to the caller to determine if any code was loaded (for example, check
* class_exists() or function_exists() for what was expected in the code).
*
* @param string $name
* The virtual file name. Can be a relative path.
*/
public function load($name);
/**
* Saves PHP code to storage.
*
* @param string $name
* The virtual file name. Can be a relative path.
* @param string $code
* The PHP code to be saved.
*
* @return bool
* TRUE if the save succeeded, FALSE if it failed.
*/
public function save($name, $code);
/**
* Whether this is a writeable storage.
*
* @return bool
*/
public function writeable();
/**
* Deletes PHP code from storage.
*
* @param string $name
* The virtual file name. Can be a relative path.
*
* @return bool
* TRUE if the delete succeeded, FALSE if it failed.
*/
public function delete($name);
/**
* Removes all files in this bin.
*/
public function deleteAll();
/**
* Gets the full file path.
*
* @param string $name
* The virtual file name. Can be a relative path.
*
* @return string|FALSE
* The full file path for the provided name. Return FALSE if the
* implementation needs to prevent access to the file.
*/
public function getFullPath($name);
/**
* Lists all the files in the storage.
*
* @return array
* Array of filenames.
*/
public function listAll();
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\CategorizingPluginManagerInterface.
*/
namespace Drupal\Component\Plugin;
/**
* Defines an interface for plugin managers that categorize plugin definitions.
*/
interface CategorizingPluginManagerInterface extends PluginManagerInterface {
/**
* Gets the names of all categories.
*
* @return string[]
* An array of translated categories, sorted alphabetically.
*/
public function getCategories();
/**
* Gets sorted plugin definitions.
*
* @param array[]|null $definitions
* (optional) The plugin definitions to sort. If omitted, all plugin
* definitions are used.
*
* @return array[]
* An array of plugin definitions, sorted by category and label.
*/
public function getSortedDefinitions(array $definitions = NULL);
/**
* Gets sorted plugin definitions grouped by category.
*
* In addition to grouping, both categories and its entries are sorted,
* whereas plugin definitions are sorted by label.
*
* @param array[]|null $definitions
* (optional) The plugin definitions to group. If omitted, all plugin
* definitions are used.
*
* @return array[]
* Keys are category names, and values are arrays of which the keys are
* plugin IDs and the values are plugin definitions.
*/
public function getGroupedDefinitions(array $definitions = NULL);
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\ConfigurablePluginInterface.
*/
namespace Drupal\Component\Plugin;
/**
* Provides an interface for a configurable plugin.
*
* @ingroup plugin_api
*/
interface ConfigurablePluginInterface extends DependentPluginInterface {
/**
* Gets this plugin's configuration.
*
* @return array
* An array of this plugin's configuration.
*/
public function getConfiguration();
/**
* Sets the configuration for this plugin instance.
*
* @param array $configuration
* An associative array containing the plugin's configuration.
*/
public function setConfiguration(array $configuration);
/**
* Gets default configuration for this plugin.
*
* @return array
* An associative array with the default configuration.
*/
public function defaultConfiguration();
}

View file

@ -0,0 +1,103 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Context\Context.
*/
namespace Drupal\Component\Plugin\Context;
use Drupal\Component\Plugin\Exception\ContextException;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Validator\Validation;
/**
* A generic context class for wrapping data a plugin needs to operate.
*/
class Context implements ContextInterface {
/**
* The value of the context.
*
* @var mixed
*/
protected $contextValue;
/**
* The definition to which a context must conform.
*
* @var \Drupal\Component\Plugin\Context\ContextDefinitionInterface
*/
protected $contextDefinition;
/**
* Sets the contextDefinition for us without needing to call the setter.
*
* @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface $context_definition
* The context definition.
*/
public function __construct(ContextDefinitionInterface $context_definition) {
$this->contextDefinition = $context_definition;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::setContextValue().
*/
public function setContextValue($value) {
$this->contextValue = $value;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::getContextValue().
*/
public function getContextValue() {
// Support optional contexts.
if (!isset($this->contextValue)) {
$definition = $this->getContextDefinition();
$default_value = $definition->getDefaultValue();
if (!isset($default_value) && $definition->isRequired()) {
$type = $definition->getDataType();
throw new ContextException(sprintf("The %s context is required and not present.", $type));
}
// Keep the default value here so that subsequent calls don't have to look
// it up again.
$this->contextValue = $default_value;
}
return $this->contextValue;
}
/**
* {@inheritdoc}
*/
public function setContextDefinition(ContextDefinitionInterface $context_definition) {
$this->contextDefinition = $context_definition;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::getContextDefinition().
*/
public function getContextDefinition() {
return $this->contextDefinition;
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::getConstraints().
*/
public function getConstraints() {
if (empty($this->contextDefinition['class'])) {
throw new ContextException("An error was encountered while trying to validate the context.");
}
return array(new Type($this->contextDefinition['class']));
}
/**
* Implements \Drupal\Component\Plugin\Context\ContextInterface::validate().
*/
public function validate() {
$validator = Validation::createValidatorBuilder()
->getValidator();
return $validator->validateValue($this->getContextValue(), $this->getConstraints());
}
}

View file

@ -0,0 +1,179 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Context\ContextDefinitionInterface.
*/
namespace Drupal\Component\Plugin\Context;
/**
* Interface for context definitions.
*
* @todo WARNING: This interface is going to receive some additions as part of
* https://www.drupal.org/node/2346999.
*/
interface ContextDefinitionInterface {
/**
* Gets a human readable label.
*
* @return string
* The label.
*/
public function getLabel();
/**
* Sets the human readable label.
*
* @param string $label
* The label to set.
*
* @return $this
*/
public function setLabel($label);
/**
* Gets a human readable description.
*
* @return string|null
* The description, or NULL if no description is available.
*/
public function getDescription();
/**
* Sets the human readable description.
*
* @param string|null $description
* The description to set.
*
* @return $this
*/
public function setDescription($description);
/**
* Gets the data type needed by the context.
*
* If the context is multiple-valued, this represents the type of each value.
*
* @return string
* The data type.
*/
public function getDataType();
/**
* Sets the data type needed by the context.
*
* @param string $data_type
* The data type to set.
*
* @return $this
*/
public function setDataType($data_type);
/**
* Determines whether the data is multi-valued, i.e. a list of data items.
*
* @return bool
* Whether the data is multi-valued; i.e. a list of data items.
*/
public function isMultiple();
/**
* Sets whether the data is multi-valued.
*
* @param bool $multiple
* (optional) Whether the data is multi-valued. Defaults to TRUE.
*
* @return $this
*/
public function setMultiple($multiple = TRUE);
/**
* Determines whether the context is required.
*
* For required data a non-NULL value is mandatory.
*
* @return bool
* Whether a data value is required.
*/
public function isRequired();
/**
* Sets whether the data is required.
*
* @param bool $required
* (optional) Whether the data is multi-valued. Defaults to TRUE.
*
* @return $this
*/
public function setRequired($required = TRUE);
/**
* Gets the default value for this context definition.
*
* @return mixed
* The default value or NULL if no default value is set.
*/
public function getDefaultValue();
/**
* Sets the default data value.
*
* @param mixed $default_value
* The default value to be set or NULL to remove any default value.
*
* @return $this
*/
public function setDefaultValue($default_value);
/**
* Gets an array of validation constraints.
*
* @return array
* An array of validation constraint definitions, keyed by constraint name.
* Each constraint definition can be used for instantiating
* \Symfony\Component\Validator\Constraint objects.
*/
public function getConstraints();
/**
* Sets the array of validation constraints.
*
* NOTE: This will override any previously set constraints. In most cases
* ContextDefinitionInterface::addConstraint() should be used instead.
*
* @param array $constraints
* The array of constraints.
*
* @return $this
*
* @see self::addConstraint()
*/
public function setConstraints(array $constraints);
/**
* Adds a validation constraint.
*
* @param string $constraint_name
* The name of the constraint to add, i.e. its plugin id.
* @param array|null $options
* The constraint options as required by the constraint plugin, or NULL.
*
* @return $this
*/
public function addConstraint($constraint_name, $options = NULL);
/**
* Gets a validation constraint.
*
* @param string $constraint_name
* The name of the constraint, i.e. its plugin id.
*
* @return array
* A validation constraint definition which can be used for instantiating a
* \Symfony\Component\Validator\Constraint object.
*/
public function getConstraint($constraint_name);
}

View file

@ -0,0 +1,68 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Context\ContextInterface.
*/
namespace Drupal\Component\Plugin\Context;
/**
* A generic context interface for wrapping data a plugin needs to operate.
*/
interface ContextInterface {
/**
* Sets the context value.
*
* @param mixed $value
* The value of this context, matching the context definition.
*
* @see \Drupal\Component\Plugin\Context\ContextInterface::setContextDefinition().
*/
public function setContextValue($value);
/**
* Gets the context value.
*
* @return mixed
* The currently set context value, or NULL if it is not set.
*/
public function getContextValue();
/**
* Sets the definition that the context must conform to.
*
* @param \Drupal\Component\Plugin\Context\ContextDefinitionInterface $context_definition
* A defining characteristic representation of the context against which
* that context can be validated.
*/
public function setContextDefinition(ContextDefinitionInterface $context_definition);
/**
* Gets the provided definition that the context must conform to.
*
* @return \Drupal\Component\Plugin\Context\ContextDefinitionInterface
* The defining characteristic representation of the context.
*/
public function getContextDefinition();
/**
* Gets a list of validation constraints.
*
* @return array
* Array of constraints, each being an instance of
* \Symfony\Component\Validator\Constraint.
*/
public function getConstraints();
/**
* Validates the set context value.
*
* @return \Symfony\Component\Validator\ConstraintViolationListInterface
* A list of constraint violations. If the list is empty, validation
* succeeded.
*/
public function validate();
}

View file

@ -0,0 +1,145 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\ContextAwarePluginBase.
*/
namespace Drupal\Component\Plugin;
use Drupal\Component\Plugin\Context\ContextInterface;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Component\Plugin\Context\Context;
use Symfony\Component\Validator\ConstraintViolationList;
/**
* Base class for plugins that are context aware.
*/
abstract class ContextAwarePluginBase extends PluginBase implements ContextAwarePluginInterface {
/**
* The data objects representing the context of this plugin.
*
* @var \Drupal\Component\Plugin\Context\ContextInterface[]
*/
protected $context;
/**
* Overrides \Drupal\Component\Plugin\PluginBase::__construct().
*
* Overrides the construction of context aware plugins to allow for
* unvalidated constructor based injection of contexts.
*
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
$context = array();
if (isset($configuration['context'])) {
$context = $configuration['context'];
unset($configuration['context']);
}
parent::__construct($configuration, $plugin_id, $plugin_definition);
foreach ($context as $key => $value) {
$context_definition = $this->getContextDefinition($key);
$this->context[$key] = new Context($context_definition);
$this->context[$key]->setContextValue($value);
}
}
/**
* {@inheritdoc}
*/
public function getContextDefinitions() {
$definition = $this->getPluginDefinition();
return !empty($definition['context']) ? $definition['context'] : array();
}
/**
* {@inheritdoc}
*/
public function getContextDefinition($name) {
$definition = $this->getPluginDefinition();
if (empty($definition['context'][$name])) {
throw new ContextException(sprintf("The %s context is not a valid context.", $name));
}
return $definition['context'][$name];
}
/**
* {@inheritdoc}
*/
public function getContexts() {
// Make sure all context objects are initialized.
foreach ($this->getContextDefinitions() as $name => $definition) {
$this->getContext($name);
}
return $this->context;
}
/**
* {@inheritdoc}
*/
public function getContext($name) {
// Check for a valid context value.
if (!isset($this->context[$name])) {
$this->context[$name] = new Context($this->getContextDefinition($name));
}
return $this->context[$name];
}
/**
* {@inheritdoc}
*/
public function setContext($name, ContextInterface $context) {
$this->context[$name] = $context;
}
/**
* {@inheritdoc}
*/
public function getContextValues() {
$values = array();
foreach ($this->getContextDefinitions() as $name => $definition) {
$values[$name] = isset($this->context[$name]) ? $this->context[$name]->getContextValue() : NULL;
}
return $values;
}
/**
* {@inheritdoc}
*/
public function getContextValue($name) {
return $this->getContext($name)->getContextValue();
}
/**
* {@inheritdoc}
*/
public function setContextValue($name, $value) {
$this->getContext($name)->setContextValue($value);
return $this;
}
/**
* {@inheritdoc}
*/
public function validateContexts() {
$violations = new ConstraintViolationList();
// @todo: Implement symfony validator API to let the validator traverse
// and set property paths accordingly.
foreach ($this->getContexts() as $context) {
$violations->addAll($context->validate());
}
return $violations;
}
}

View file

@ -0,0 +1,152 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\ContextAwarePluginInterface.
*/
namespace Drupal\Component\Plugin;
use \Drupal\Component\Plugin\Context\ContextInterface;
/**
* Interface for defining context aware plugins.
*
* Context aware plugins can specify an array of context definitions keyed by
* context name at the plugin definition under the "context" key.
*
* @ingroup plugin_api
*/
interface ContextAwarePluginInterface extends PluginInspectionInterface {
/**
* Gets the context definitions of the plugin.
*
* @return \Drupal\Component\Plugin\Context\ContextDefinitionInterface[]
* The array of context definitions, keyed by context name.
*/
public function getContextDefinitions();
/**
* Gets a specific context definition of the plugin.
*
* @param string $name
* The name of the context in the plugin definition.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the requested context is not defined.
*
* @return \Drupal\Component\Plugin\Context\ContextDefinitionInterface.
* The definition against which the context value must validate.
*/
public function getContextDefinition($name);
/**
* Gets the defined contexts.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If contexts are defined but not set.
*
* @return array
* The set context objects.
*/
public function getContexts();
/**
* Gets a defined context.
*
* @param string $name
* The name of the context in the plugin definition.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the requested context is not set.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface
* The context object.
*/
public function getContext($name);
/**
* Gets the values for all defined contexts.
*
* @return array
* An array of set context values, keyed by context name. If a context is
* unset its value is returned as NULL.
*/
public function getContextValues();
/**
* Gets the value for a defined context.
*
* @param string $name
* The name of the context in the plugin configuration.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the requested context is not set.
*
* @return mixed
* The currently set context value.
*/
public function getContextValue($name);
/**
* Set a context on this plugin.
*
* @param string $name
* The name of the context in the plugin configuration.
* @param \Drupal\Component\Plugin\Context\ContextInterface $context
* The context object to set.
*/
public function setContext($name, ContextInterface $context);
/**
* Sets the value for a defined context.
*
* @param string $name
* The name of the context in the plugin definition.
* @param mixed $value
* The value to set the context to. The value has to validate against the
* provided context definition.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the value does not pass validation.
*
* @return \Drupal\Component\Plugin\ContextAwarePluginInterface.
* A context aware plugin object for chaining.
*/
public function setContextValue($name, $value);
/**
* Validates the set values for the defined contexts.
*
* @return \Symfony\Component\Validator\ConstraintViolationListInterface
* A list of constraint violations. If the list is empty, validation
* succeeded.
*/
public function validateContexts();
/**
* Gets a mapping of the expected assignment names to their context names.
*
* @return array
* A mapping of the expected assignment names to their context names. For
* example, if one of the $contexts is named 'user.current_user', but the
* plugin expects a context named 'user', then this map would contain
* 'user' => 'user.current_user'.
*/
public function getContextMapping();
/**
* Sets a mapping of the expected assignment names to their context names.
*
* @param array $context_mapping
* A mapping of the expected assignment names to their context names. For
* example, if one of the $contexts is named 'user.current_user', but the
* plugin expects a context named 'user', then this map would contain
* 'user' => 'user.current_user'.
*
* @return $this
*/
public function setContextMapping(array $context_mapping);
}

View file

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\DependentPluginInterface.
*/
namespace Drupal\Component\Plugin;
/**
* Provides an interface for a plugin that has dependencies.
*
* @ingroup plugin_api
*/
interface DependentPluginInterface {
/**
* Calculates dependencies for the configured plugin.
*
* Dependencies are saved in the plugin's configuration entity and are used to
* determine configuration synchronization order. For example, if the plugin
* integrates with specific user roles, this method should return an array of
* dependencies listing the specified roles.
*
* @return array
* An array of dependencies grouped by type (config, content, module,
* theme). For example:
* @code
* array(
* 'config' => array('user.role.anonymous', 'user.role.authenticated'),
* 'content' => array('node:article:f0a189e6-55fb-47fb-8005-5bef81c44d6d'),
* 'module' => array('node', 'user'),
* 'theme' => array('seven'),
* );
* @endcode
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
* @see \Drupal\Core\Entity\EntityInterface::getConfigDependencyName()
*/
public function calculateDependencies();
}

View file

@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Derivative\DeriverBase.
*/
namespace Drupal\Component\Plugin\Derivative;
/**
* Provides a basic deriver.
*/
abstract class DeriverBase implements DeriverInterface {
/**
* List of derivative definitions.
*
* @var array
*/
protected $derivatives = array();
/**
* {@inheritdoc}
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
if (!empty($this->derivatives) && !empty($this->derivatives[$derivative_id])) {
return $this->derivatives[$derivative_id];
}
$this->getDerivativeDefinitions($base_plugin_definition);
return $this->derivatives[$derivative_id];
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
return $this->derivatives;
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Derivative\DeriverInterface.
*/
namespace Drupal\Component\Plugin\Derivative;
/**
* Provides additional plugin definitions based on an existing definition.
*
* @ingroup plugin_api
*/
interface DeriverInterface {
/**
* Gets the definition of a derivative plugin.
*
* @param string $derivative_id
* The derivative id. The id must uniquely identify the derivative within a
* given base plugin, but derivative ids can be reused across base plugins.
* @param mixed $base_plugin_definition
* The definition of the base plugin from which the derivative plugin
* is derived. It is maybe an entire object or just some array, depending
* on the discovery mechanism.
*
* @return array
* The full definition array of the derivative plugin, typically a merge of
* $base_plugin_definition with extra derivative-specific information. NULL
* if the derivative doesn't exist.
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition);
/**
* Gets the definition of all derivatives of a base plugin.
*
* @param array $base_plugin_definition
* The definition array of the base plugin.
* @return array
* An array of full derivative definitions keyed on derivative id.
*
* @see getDerivativeDefinition()
*/
public function getDerivativeDefinitions($base_plugin_definition);
}

View file

@ -0,0 +1,31 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\DerivativeInspectionInterface.
*/
namespace Drupal\Component\Plugin;
/**
* Provides a plugin interface for providing derivative metadata inspection.
*/
interface DerivativeInspectionInterface {
/**
* Gets the base_plugin_id of the plugin instance.
*
* @return string
* The base_plugin_id of the plugin instance.
*/
public function getBaseId();
/**
* Gets the derivative_id of the plugin instance.
*
* @return string|null
* The derivative_id of the plugin instance NULL otherwise.
*/
public function getDerivativeId();
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface.
*/
namespace Drupal\Component\Plugin\Discovery;
/**
* Interface for discovery components holding a cache of plugin definitions.
*/
interface CachedDiscoveryInterface extends DiscoveryInterface {
/**
* Clears static and persistent plugin definition caches.
*
* Don't resort to calling \Drupal::cache()->delete() and friends to make
* Drupal detect new or updated plugin definitions. Always use this method on
* the appropriate plugin type's plugin manager!
*/
public function clearCachedDefinitions();
/**
* Disable the use of caches.
*
* Can be used to ensure that uncached plugin definitions are returned,
* without invalidating all cached information.
*
* This will also remove all local/static caches.
*
* @param bool $use_caches
* FALSE to not use any caches.
*/
public function useCaches($use_caches = FALSE);
}

View file

@ -0,0 +1,248 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator.
*/
namespace Drupal\Component\Plugin\Discovery;
use Drupal\Component\Plugin\Exception\InvalidDeriverException;
/**
* Base class providing the tools for a plugin discovery to be derivative aware.
*
* Provides a decorator that allows the use of plugin derivatives for normal
* implementations DiscoveryInterface.
*/
class DerivativeDiscoveryDecorator implements DiscoveryInterface {
use DiscoveryTrait;
/**
* Plugin derivers.
*
* @var \Drupal\Component\Plugin\Derivative\DeriverInterface[]
* Keys are base plugin IDs.
*/
protected $derivers = array();
/**
* The decorated plugin discovery.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $decorated;
/**
* Creates a new instance.
*
* @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
* The parent object implementing DiscoveryInterface that is being
* decorated.
*/
public function __construct(DiscoveryInterface $decorated) {
$this->decorated = $decorated;
}
/**
* {@inheritdoc}
*
* @throws \Drupal\Component\Plugin\Exception\InvalidDeriverException
* Thrown if the 'deriver' class specified in the plugin definition
* does not implement \Drupal\Component\Plugin\Derivative\DeriverInterface.
*/
public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
// This check is only for derivative plugins that have explicitly provided
// an ID. This is not common, and can be expected to fail. Therefore, opt
// out of the thrown exception, which will be handled when checking the
// $base_plugin_id.
$plugin_definition = $this->decorated->getDefinition($plugin_id, FALSE);
list($base_plugin_id, $derivative_id) = $this->decodePluginId($plugin_id);
$base_plugin_definition = $this->decorated->getDefinition($base_plugin_id, $exception_on_invalid);
if ($base_plugin_definition) {
$deriver = $this->getDeriver($base_plugin_id, $base_plugin_definition);
if ($deriver) {
$derivative_plugin_definition = $deriver->getDerivativeDefinition($derivative_id, $base_plugin_definition);
// If a plugin defined itself as a derivative, merge in possible
// defaults from the derivative.
if ($derivative_id && isset($plugin_definition)) {
$plugin_definition = $this->mergeDerivativeDefinition($plugin_definition, $derivative_plugin_definition);
}
else {
$plugin_definition = $derivative_plugin_definition;
}
}
}
return $plugin_definition;
}
/**
* {@inheritdoc}
*
* @throws \Drupal\Component\Plugin\Exception\InvalidDeriverException
* Thrown if the 'deriver' class specified in the plugin definition
* does not implement \Drupal\Component\Plugin\Derivative\DeriverInterface.
*/
public function getDefinitions() {
$plugin_definitions = $this->decorated->getDefinitions();
return $this->getDerivatives($plugin_definitions);
}
/**
* Adds derivatives to a list of plugin definitions.
*
* This should be called by the class extending this in
* DiscoveryInterface::getDefinitions().
*/
protected function getDerivatives(array $base_plugin_definitions) {
$plugin_definitions = array();
foreach ($base_plugin_definitions as $base_plugin_id => $plugin_definition) {
$deriver = $this->getDeriver($base_plugin_id, $plugin_definition);
if ($deriver) {
$derivative_definitions = $deriver->getDerivativeDefinitions($plugin_definition);
foreach ($derivative_definitions as $derivative_id => $derivative_definition) {
$plugin_id = $this->encodePluginId($base_plugin_id, $derivative_id);
// Use this definition as defaults if a plugin already defined
// itself as this derivative.
if ($derivative_id && isset($base_plugin_definitions[$plugin_id])) {
$derivative_definition = $this->mergeDerivativeDefinition($base_plugin_definitions[$plugin_id], $derivative_definition);
}
$plugin_definitions[$plugin_id] = $derivative_definition;
}
}
// If a plugin already defined itself as a derivative it might already
// be merged into the definitions.
elseif (!isset($plugin_definitions[$base_plugin_id])) {
$plugin_definitions[$base_plugin_id] = $plugin_definition;
}
}
return $plugin_definitions;
}
/**
* Decodes derivative id and plugin id from a string.
*
* @param string $plugin_id
* Plugin identifier that may point to a derivative plugin.
*
* @return array
* An array with the base plugin id as the first index and the derivative id
* as the second. If there is no derivative id it will be null.
*/
protected function decodePluginId($plugin_id) {
// Try and split the passed plugin definition into a plugin and a
// derivative id. We don't need to check for !== FALSE because a leading
// colon would break the derivative system and doesn't makes sense.
if (strpos($plugin_id, ':')) {
return explode(':', $plugin_id, 2);
}
return array($plugin_id, NULL);
}
/**
* Encodes plugin and derivative id's into a string.
*
* @param string $base_plugin_id
* The base plugin identifier.
* @param string $derivative_id
* The derivative identifier.
*
* @return string
* A uniquely encoded combination of the $base_plugin_id and $derivative_id.
*/
protected function encodePluginId($base_plugin_id, $derivative_id) {
if ($derivative_id) {
return "$base_plugin_id:$derivative_id";
}
// By returning the unmerged plugin_id, we are able to support derivative
// plugins that support fetching the base definitions.
return $base_plugin_id;
}
/**
* Gets a deriver for a base plugin.
*
* @param string $base_plugin_id
* The base plugin id of the plugin.
* @param mixed $base_definition
* The base plugin definition to build derivatives.
*
* @return \Drupal\Component\Plugin\Derivative\DeriverInterface|null
* A DerivativeInterface or NULL if none exists for the plugin.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidDeriverException
* Thrown if the 'deriver' class specified in the plugin definition
* does not implement \Drupal\Component\Plugin\Derivative\DeriverInterface.
*/
protected function getDeriver($base_plugin_id, $base_definition) {
if (!isset($this->derivers[$base_plugin_id])) {
$this->derivers[$base_plugin_id] = FALSE;
$class = $this->getDeriverClass($base_definition);
if ($class) {
$this->derivers[$base_plugin_id] = new $class($base_plugin_id);
}
}
return $this->derivers[$base_plugin_id] ?: NULL;
}
/**
* Gets the deriver class name from the base plugin definition.
*
* @param array $base_definition
* The base plugin definition to build derivatives.
*
* @return string|null
* The name of a class implementing
* \Drupal\Component\Plugin\Derivative\DeriverInterface.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidDeriverException
* Thrown if the 'deriver' class specified in the plugin definition
* does not implement
* \Drupal\Component\Plugin\Derivative\DerivativeInterface.
*/
protected function getDeriverClass($base_definition) {
$class = NULL;
if ((is_array($base_definition) || ($base_definition = (array) $base_definition)) && (isset($base_definition['deriver']) && $class = $base_definition['deriver'])) {
if (!class_exists($class)) {
throw new InvalidDeriverException(sprintf('Plugin (%s) deriver "%s" does not exist.', $base_definition['id'], $class));
}
if (!is_subclass_of($class, '\Drupal\Component\Plugin\Derivative\DeriverInterface')) {
throw new InvalidDeriverException(sprintf('Plugin (%s) deriver "%s" must implement \Drupal\Component\Plugin\Derivative\DeriverInterface.', $base_definition['id'], $class));
}
}
return $class;
}
/**
* Merges a base and derivative definition, taking into account empty values.
*
* @param array $base_plugin_definition
* The base plugin definition.
* @param array $derivative_definition
* The derivative plugin definition.
*
* @return array
* The merged definition.
*/
protected function mergeDerivativeDefinition($base_plugin_definition, $derivative_definition) {
// Use this definition as defaults if a plugin already defined itself as
// this derivative, but filter out empty values first.
$filtered_base = array_filter($base_plugin_definition);
$derivative_definition = $filtered_base + ($derivative_definition ?: array());
// Add back any empty keys that the derivative didn't have.
return $derivative_definition + $base_plugin_definition;
}
/**
* Passes through all unknown calls onto the decorated object.
*/
public function __call($method, $args) {
return call_user_func_array(array($this->decorated, $method), $args);
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait.
*/
namespace Drupal\Component\Plugin\Discovery;
trait DiscoveryCachedTrait {
use DiscoveryTrait;
/**
* Cached definitions array.
*
* @var array
*/
protected $definitions;
/**
* {@inheritdoc}
*/
public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
// Fetch definitions if they're not loaded yet.
if (!isset($this->definitions)) {
$this->getDefinitions();
}
return $this->doGetDefinition($this->definitions, $plugin_id, $exception_on_invalid);
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Discovery\DiscoveryInterface.
*/
namespace Drupal\Component\Plugin\Discovery;
/**
* An interface defining the minimum requirements of building a plugin
* discovery component.
*
* @ingroup plugin_api
*/
interface DiscoveryInterface {
/**
* Gets a specific plugin definition.
*
* @param string $plugin_id
* A plugin id.
* @param bool $exception_on_invalid
* (optional) If TRUE, an invalid plugin ID will throw an exception.
*
* @return mixed
* A plugin definition, or NULL if the plugin ID is invalid and
* $exception_on_invalid is FALSE.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* Thrown if $plugin_id is invalid and $exception_on_invalid is TRUE.
*/
public function getDefinition($plugin_id, $exception_on_invalid = TRUE);
/**
* Gets the definition of all plugins for this type.
*
* @return mixed[]
* An array of plugin definitions (empty array if no definitions were
* found). Keys are plugin IDs.
*/
public function getDefinitions();
/**
* Indicates if a specific plugin definition exists.
*
* @param string $plugin_id
* A plugin ID.
*
* @return bool
* TRUE if the definition exists, FALSE otherwise.
*/
public function hasDefinition($plugin_id);
}

View file

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Discovery\DiscoveryTrait.
*/
namespace Drupal\Component\Plugin\Discovery;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
/**
* @see Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
trait DiscoveryTrait {
/**
* {@inheritdoc}
*/
abstract public function getDefinitions();
/**
* {@inheritdoc}
*/
public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
$definitions = $this->getDefinitions();
return $this->doGetDefinition($definitions, $plugin_id, $exception_on_invalid);
}
/**
* Gets a specific plugin definition.
*
* @param array $definitions
* An array of the available plugin definitions.
* @param string $plugin_id
* A plugin id.
* @param bool $exception_on_invalid
* (optional) If TRUE, an invalid plugin ID will throw an exception.
* Defaults to FALSE.
*
* @return array|null
* A plugin definition, or NULL if the plugin ID is invalid and
* $exception_on_invalid is TRUE.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* Thrown if $plugin_id is invalid and $exception_on_invalid is TRUE.
*/
protected function doGetDefinition(array $definitions, $plugin_id, $exception_on_invalid) {
// Avoid using a ternary that would create a copy of the array.
if (isset($definitions[$plugin_id])) {
return $definitions[$plugin_id];
}
elseif (!$exception_on_invalid) {
return NULL;
}
throw new PluginNotFoundException($plugin_id, sprintf('The "%s" plugin does not exist.', $plugin_id));
}
/**
* {@inheritdoc}
*/
public function hasDefinition($plugin_id) {
return (bool) $this->getDefinition($plugin_id, FALSE);
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Discovery\StaticDiscovery.
*/
namespace Drupal\Component\Plugin\Discovery;
/**
* A discovery mechanism that allows plugin definitions to be manually
* registered rather than actively discovered.
*/
class StaticDiscovery implements DiscoveryInterface {
use DiscoveryCachedTrait;
/**
* Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinitions().
*/
public function getDefinitions() {
if (!$this->definitions) {
$this->definitions = array();
}
return $this->definitions;
}
/**
* Sets a plugin definition.
*/
public function setDefinition($plugin, $definition) {
$this->definitions[$plugin] = $definition;
}
/**
* Deletes a plugin definition.
*/
public function deleteDefinition($plugin) {
unset($this->definitions[$plugin]);
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Discovery\StaticDiscoveryDecorator.
*/
namespace Drupal\Component\Plugin\Discovery;
/**
* A decorator that allows manual registration of undiscoverable definitions.
*/
class StaticDiscoveryDecorator extends StaticDiscovery {
/**
* The Discovery object being decorated.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $decorated;
/**
* A callback or closure used for registering additional definitions.
*
* @var \Callable
*/
protected $registerDefinitions;
/**
* Constructs a \Drupal\Component\Plugin\Discovery\StaticDiscoveryDecorator object.
*
* @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
* The discovery object that is being decorated.
* @param \Callable $registerDefinitions
* (optional) A callback or closure used for registering additional
* definitions.
*/
public function __construct(DiscoveryInterface $decorated, $registerDefinitions = NULL) {
$this->decorated = $decorated;
$this->registerDefinitions = $registerDefinitions;
}
/**
* {@inheritdoc}
*/
public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) {
if (isset($this->registerDefinitions)) {
call_user_func($this->registerDefinitions);
}
$this->definitions += $this->decorated->getDefinitions();
return parent::getDefinition($base_plugin_id, $exception_on_invalid);
}
/**
* Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinitions().
*/
public function getDefinitions() {
if (isset($this->registerDefinitions)) {
call_user_func($this->registerDefinitions);
}
$this->definitions += $this->decorated->getDefinitions();
return parent::getDefinitions();
}
/**
* Passes through all unknown calls onto the decorated object
*/
public function __call($method, $args) {
return call_user_func_array(array($this->decorated, $method), $args);
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Exception\ContextException.
*/
namespace Drupal\Component\Plugin\Exception;
/**
* An exception class to be thrown for context plugin exceptions.
*/
class ContextException extends \Exception implements ExceptionInterface { }

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Exception\ExceptionInterface.
*/
namespace Drupal\Component\Plugin\Exception;
/**
* Exception interface for all exceptions thrown by the Plugin component.
*/
interface ExceptionInterface { }

View file

@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Exception\InvalidDecoratedMethod.
*/
namespace Drupal\Component\Plugin\Exception;
use Drupal\Component\Plugin\Exception\ExceptionInterface;
use \BadMethodCallException;
/**
* Exception thrown when a decorator's _call() method is triggered, but the
* decorated object does not contain the requested method.
*
*/
class InvalidDecoratedMethod extends BadMethodCallException implements ExceptionInterface { }

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Exception\InvalidDeriverException.
*/
namespace Drupal\Component\Plugin\Exception;
/**
* Exception to be thrown if a plugin tries to use an invalid deriver.
*/
class InvalidDeriverException extends PluginException { }

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException.
*/
namespace Drupal\Component\Plugin\Exception;
/**
* Defines a class for invalid plugin definition exceptions.
*/
class InvalidPluginDefinitionException extends PluginException {
/**
* The plugin ID of the mapper.
*
* @var string
*/
protected $pluginId;
/**
* Constructs a InvalidPluginDefinitionException.
*
* @param string $plugin_id
* The plugin ID of the mapper.
*
* @see \Exception for the remaining parameters.
*/
public function __construct($plugin_id, $message = '', $code = 0, \Exception $previous = NULL) {
$this->pluginId = $plugin_id;
parent::__construct($message, $code, $previous);
}
/**
* Gets the plugin ID of the mapper that raised the exception.
*
* @return string
* The plugin ID.
*/
public function getPluginId() {
return $this->pluginId;
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Exception\MapperExceptionInterface.
*
* Base exception interface for grouping mapper exceptions.
*/
namespace Drupal\Component\Plugin\Exception;
/**
* Extended interface for exceptions thrown specifically by the Mapper subsystem
* within the Plugin component.
*/
interface MapperExceptionInterface extends ExceptionInterface { }

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Exception\PluginException.
*/
namespace Drupal\Component\Plugin\Exception;
/**
* Generic Plugin exception class to be thrown when no more specific class
* is applicable.
*/
class PluginException extends \Exception implements ExceptionInterface { }

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Exception\PluginNotFoundException.
*/
namespace Drupal\Component\Plugin\Exception;
/**
* Plugin exception class to be thrown when a plugin ID could not be found.
*/
class PluginNotFoundException extends PluginException {
/**
* Construct an PluginNotFoundException exception.
*
* @param string $plugin_id
* The plugin ID that was not found.
*
* @see \Exception for remaining parameters.
*/
public function __construct($plugin_id, $message = '', $code = 0, \Exception $previous = NULL) {
if (empty($message)) {
$message = sprintf("Plugin ID '%s' was not found.", $plugin_id);
}
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,96 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Factory\DefaultFactory.
*/
namespace Drupal\Component\Plugin\Factory;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Plugin\Exception\PluginException;
/**
* Default plugin factory.
*
* Instantiates plugin instances by passing the full configuration array as a
* single constructor argument. Plugin types wanting to support plugin classes
* with more flexible constructor signatures can do so by using an alternate
* factory such as Drupal\Component\Plugin\Factory\ReflectionFactory.
*/
class DefaultFactory implements FactoryInterface {
/**
* The object that retrieves the definitions of the plugins that this factory instantiates.
*
* The plugin definition includes the plugin class and possibly other
* information necessary for proper instantiation.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $discovery;
/**
* Defines an interface each plugin should implement.
*
* @var string|null
*/
protected $interface;
/**
* Constructs a Drupal\Component\Plugin\Factory\DefaultFactory object.
*
* @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $discovery
* The plugin discovery.
* @param string|null $plugin_interface
* (optional) The interface each plugin should implement.
*/
public function __construct(DiscoveryInterface $discovery, $plugin_interface = NULL) {
$this->discovery = $discovery;
$this->interface = $plugin_interface;
}
/**
* Implements Drupal\Component\Plugin\Factory\FactoryInterface::createInstance().
*/
public function createInstance($plugin_id, array $configuration = array()) {
$plugin_definition = $this->discovery->getDefinition($plugin_id);
$plugin_class = static::getPluginClass($plugin_id, $plugin_definition, $this->interface);
return new $plugin_class($configuration, $plugin_id, $plugin_definition);
}
/**
* Finds the class relevant for a given plugin.
*
* @param string $plugin_id
* The id of a plugin.
* @param mixed $plugin_definition
* The plugin definition associated with the plugin ID.
* @param string $required_interface
* (optional) THe required plugin interface.
*
* @return string
* The appropriate class name.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown when there is no class specified, the class doesn't exist, or
* the class does not implement the specified required interface.
*
*/
public static function getPluginClass($plugin_id, $plugin_definition = NULL, $required_interface = NULL) {
if (empty($plugin_definition['class'])) {
throw new PluginException(sprintf('The plugin (%s) did not specify an instance class.', $plugin_id));
}
$class = $plugin_definition['class'];
if (!class_exists($class)) {
throw new PluginException(sprintf('Plugin (%s) instance class "%s" does not exist.', $plugin_id, $class));
}
if ($required_interface && !is_subclass_of($plugin_definition['class'], $required_interface)) {
throw new PluginException(sprintf('Plugin "%s" (%s) must implement interface %s.', $plugin_id, $plugin_definition['class'], $required_interface));
}
return $class;
}
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Factory\FactoryInterface.
*/
namespace Drupal\Component\Plugin\Factory;
/**
* Factory interface implemented by all plugin factories.
*/
interface FactoryInterface {
/**
* Creates a pre-configured instance of a plugin.
*
* @param string $plugin_id
* The ID of the plugin being instantiated.
* @param array $configuration
* An array of configuration relevant to the plugin instance.
*
* @return object
* A fully configured plugin instance.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the instance cannot be created, such as if the ID is invalid.
*/
public function createInstance($plugin_id, array $configuration = array());
}

View file

@ -0,0 +1,83 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Factory\ReflectionFactory.
*/
namespace Drupal\Component\Plugin\Factory;
/**
* A plugin factory that maps instance configuration to constructor arguments.
*
* Provides logic for any basic plugin type that needs to provide individual
* plugins based upon some basic logic.
*/
class ReflectionFactory extends DefaultFactory {
/**
* Implements Drupal\Component\Plugin\Factory\FactoryInterface::createInstance().
*/
public function createInstance($plugin_id, array $configuration = array()) {
$plugin_definition = $this->discovery->getDefinition($plugin_id);
$plugin_class = static::getPluginClass($plugin_id, $plugin_definition, $this->interface);
// Lets figure out of there's a constructor for this class and pull
// arguments from the $options array if so to populate it.
$reflector = new \ReflectionClass($plugin_class);
if ($reflector->hasMethod('__construct')) {
$arguments = $this->getInstanceArguments($reflector, $plugin_id, $plugin_definition, $configuration);
$instance = $reflector->newInstanceArgs($arguments);
}
else {
$instance = new $plugin_class();
}
return $instance;
}
/**
* Inspects the plugin class and build a list of arguments for the constructor.
*
* This is provided as a helper method so factories extending this class can
* replace this and insert their own reflection logic.
*
* @param \ReflectionClass $reflector
* The reflector object being used to inspect the plugin class.
* @param string $plugin_id
* The identifier of the plugin implementation.
* @param mixed $plugin_definition
* The definition associated to the plugin_id.
* @param array $configuration
* An array of configuration that may be passed to the instance.
*
* @return array
* An array of arguments to be passed to the constructor.
*/
protected function getInstanceArguments(\ReflectionClass $reflector, $plugin_id, $plugin_definition, array $configuration) {
$arguments = array();
foreach ($reflector->getMethod('__construct')->getParameters() as $param) {
$param_name = $param->getName();
if ($param_name == 'plugin_id') {
$arguments[] = $plugin_id;
}
elseif ($param_name == 'plugin_definition') {
$arguments[] = $plugin_definition;
}
elseif ($param_name == 'configuration') {
$arguments[] = $configuration;
}
elseif (isset($configuration[$param_name]) || array_key_exists($param_name, $configuration)) {
$arguments[] = $configuration[$param_name];
}
elseif ($param->isDefaultValueAvailable()) {
$arguments[] = $param->getDefaultValue();
}
else {
$arguments[] = NULL;
}
}
return $arguments;
}
}

View file

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\FallbackPluginManagerInterface.
*/
namespace Drupal\Component\Plugin;
/**
* An interface implemented by plugin managers with fallback plugin behaviors.
*/
interface FallbackPluginManagerInterface {
/**
* Gets a fallback id for a missing plugin.
*
* @param string $plugin_id
* The ID of the missing requested plugin.
* @param array $configuration
* An array of configuration relevant to the plugin instance.
*
* @return string
* The id of an existing plugin to use when the plugin does not exist.
*/
public function getFallbackPluginId($plugin_id, array $configuration = array());
}

View file

@ -0,0 +1,165 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\LazyPluginCollection.
*/
namespace Drupal\Component\Plugin;
/**
* Defines an object which stores multiple plugin instances to lazy load them.
*
* @ingroup plugin_api
*/
abstract class LazyPluginCollection implements \IteratorAggregate, \Countable {
/**
* Stores all instantiated plugins.
*
* @var array
*/
protected $pluginInstances = array();
/**
* Stores the IDs of all potential plugin instances.
*
* @var array
*/
protected $instanceIDs = array();
/**
* Initializes and stores a plugin.
*
* @param string $instance_id
* The ID of the plugin instance to initialize.
*/
abstract protected function initializePlugin($instance_id);
/**
* Gets the current configuration of all plugins in this collection.
*
* @return array
* An array of up-to-date plugin configuration.
*/
abstract public function getConfiguration();
/**
* Sets the configuration for all plugins in this collection.
*
* @param array $configuration
* An array of up-to-date plugin configuration.
*
* @return $this
*/
abstract public function setConfiguration($configuration);
/**
* Clears all instantiated plugins.
*/
public function clear() {
$this->pluginInstances = array();
}
/**
* Determines if a plugin instance exists.
*
* @param string $instance_id
* The ID of the plugin instance to check.
*
* @return bool
* TRUE if the plugin instance exists, FALSE otherwise.
*/
public function has($instance_id) {
return isset($this->pluginInstances[$instance_id]) || isset($this->instanceIDs[$instance_id]);
}
/**
* Gets a plugin instance, initializing it if necessary.
*
* @param string $instance_id
* The ID of the plugin instance being retrieved.
*/
public function &get($instance_id) {
if (!isset($this->pluginInstances[$instance_id])) {
$this->initializePlugin($instance_id);
}
return $this->pluginInstances[$instance_id];
}
/**
* Stores an initialized plugin.
*
* @param string $instance_id
* The ID of the plugin instance being stored.
* @param mixed $value
* An instantiated plugin.
*/
public function set($instance_id, $value) {
$this->pluginInstances[$instance_id] = $value;
$this->addInstanceId($instance_id);
}
/**
* Removes an initialized plugin.
*
* The plugin can still be used; it will be reinitialized.
*
* @param string $instance_id
* The ID of the plugin instance to remove.
*/
public function remove($instance_id) {
unset($this->pluginInstances[$instance_id]);
}
/**
* Adds an instance ID to the available instance IDs.
*
* @param string $id
* The ID of the plugin instance to add.
* @param array|null $configuration
* (optional) The configuration used by this instance. Defaults to NULL.
*/
public function addInstanceId($id, $configuration = NULL) {
if (!isset($this->instanceIDs[$id])) {
$this->instanceIDs[$id] = $id;
}
}
/**
* Gets all instance IDs.
*
* @return array
* An array of all available instance IDs.
*/
public function getInstanceIds() {
return $this->instanceIDs;
}
/**
* Removes an instance ID.
*
* @param string $instance_id
* The ID of the plugin instance to remove.
*/
public function removeInstanceId($instance_id) {
unset($this->instanceIDs[$instance_id]);
$this->remove($instance_id);
}
public function getIterator() {
$instances = [];
foreach ($this->getInstanceIds() as $instance_id) {
$instances[$instance_id] = $this->get($instance_id);
}
return new \ArrayIterator($instances);
}
/**
* {@inheritdoc}
*/
public function count() {
return count($this->instanceIDs);
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Mapper\MapperInterface.
*/
namespace Drupal\Component\Plugin\Mapper;
/**
* Plugin mapper interface.
*
* Plugin mappers are responsible for mapping a plugin request to its
* implementation. For example, it might map a cache bin to a memcache bin.
*
* Mapper objects incorporate the best practices of retrieving configurations,
* type information, and factory instantiation.
*/
interface MapperInterface {
/**
* Gets a preconfigured instance of a plugin.
*
* @param array $options
* An array of options that can be used to determine a suitable plugin to
* instantiate and how to configure it.
*
* @return object|false
* A fully configured plugin instance. The interface of the plugin instance
* will depends on the plugin type. If no instance can be retrieved, FALSE
* will be returned.
*/
public function getInstance(array $options);
}

View file

@ -0,0 +1,99 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\PluginBase.
*/
namespace Drupal\Component\Plugin;
/**
* Base class for plugins wishing to support metadata inspection.
*/
abstract class PluginBase implements PluginInspectionInterface, DerivativeInspectionInterface {
/**
* A string which is used to separate base plugin IDs from the derivative ID.
*/
const DERIVATIVE_SEPARATOR = ':';
/**
* The plugin_id.
*
* @var string
*/
protected $pluginId;
/**
* The plugin implementation definition.
*
* @var array
*/
protected $pluginDefinition;
/**
* Configuration information passed into the plugin.
*
* When using an interface like
* \Drupal\Component\Plugin\ConfigurablePluginInterface, this is where the
* configuration should be stored.
*
* Plugin configuration is optional, so plugin implementations must provide
* their own setters and getters.
*
* @var array
*/
protected $configuration;
/**
* Constructs a Drupal\Component\Plugin\PluginBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
$this->configuration = $configuration;
$this->pluginId = $plugin_id;
$this->pluginDefinition = $plugin_definition;
}
/**
* {@inheritdoc}
*/
public function getPluginId() {
return $this->pluginId;
}
/**
* {@inheritdoc}
*/
public function getBaseId() {
$plugin_id = $this->getPluginId();
if (strpos($plugin_id, static::DERIVATIVE_SEPARATOR)) {
list($plugin_id) = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 2);
}
return $plugin_id;
}
/**
* {@inheritdoc}
*/
public function getDerivativeId() {
$plugin_id = $this->getPluginId();
$derivative_id = NULL;
if (strpos($plugin_id, static::DERIVATIVE_SEPARATOR)) {
list(, $derivative_id) = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 2);
}
return $derivative_id;
}
/**
* {@inheritdoc}
*/
public function getPluginDefinition() {
return $this->pluginDefinition;
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\PluginInspectionInterface.
*/
namespace Drupal\Component\Plugin;
/**
* Plugin interface for providing some metadata inspection.
*
* This interface provides some simple tools for code receiving a plugin to
* interact with the plugin system.
*
* @ingroup plugin_api
*/
interface PluginInspectionInterface {
/**
* Gets the plugin_id of the plugin instance.
*
* @return string
* The plugin_id of the plugin instance.
*/
public function getPluginId();
/**
* Gets the definition of the plugin implementation.
*
* @return array
* The plugin definition, as returned by the discovery object used by the
* plugin manager.
*/
public function getPluginDefinition();
}

View file

@ -0,0 +1,100 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\PluginManagerBase.
*/
namespace Drupal\Component\Plugin;
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
/**
* Base class for plugin managers.
*/
abstract class PluginManagerBase implements PluginManagerInterface {
use DiscoveryTrait;
/**
* The object that discovers plugins managed by this manager.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $discovery;
/**
* The object that instantiates plugins managed by this manager.
*
* @var \Drupal\Component\Plugin\Factory\FactoryInterface
*/
protected $factory;
/**
* The object that returns the preconfigured plugin instance appropriate for a particular runtime condition.
*
* @var \Drupal\Component\Plugin\Mapper\MapperInterface
*/
protected $mapper;
/**
* Gets the plugin discovery.
*
* @return \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected function getDiscovery() {
return $this->discovery;
}
/**
* Gets the plugin factory.
*
* @return \Drupal\Component\Plugin\Factory\FactoryInterface
*/
protected function getFactory() {
return $this->factory;
}
/**
* {@inheritdoc}
*/
public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
return $this->getDiscovery()->getDefinition($plugin_id, $exception_on_invalid);
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
return $this->getDiscovery()->getDefinitions();
}
/**
* {@inheritdoc}
*/
public function createInstance($plugin_id, array $configuration = array()) {
// If this PluginManager has fallback capabilities catch
// PluginNotFoundExceptions.
if ($this instanceof FallbackPluginManagerInterface) {
try {
return $this->getFactory()->createInstance($plugin_id, $configuration);
}
catch (PluginNotFoundException $e) {
$fallback_id = $this->getFallbackPluginId($plugin_id, $configuration);
return $this->getFactory()->createInstance($fallback_id, $configuration);
}
}
else {
return $this->getFactory()->createInstance($plugin_id, $configuration);
}
}
/**
* {@inheritdoc}
*/
public function getInstance(array $options) {
return $this->mapper->getInstance($options);
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\PluginManagerInterface.
*/
namespace Drupal\Component\Plugin;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Plugin\Factory\FactoryInterface;
use Drupal\Component\Plugin\Mapper\MapperInterface;
/**
* Interface implemented by plugin managers.
*
* There are no explicit methods on the manager interface. Instead plugin
* managers broker the interactions of the different plugin components, and
* therefore, must implement each component interface, which is enforced by
* this interface extending all of the component ones.
*
* While a plugin manager may directly implement these interface methods with
* custom logic, it is expected to be more common for plugin managers to proxy
* the method invocations to the respective components, and directly implement
* only the additional functionality needed by the specific pluggable system.
* To follow this pattern, plugin managers can extend from the PluginManagerBase
* class, which contains the proxying logic.
*
* @see \Drupal\Component\Plugin\PluginManagerBase
*
* @ingroup plugin_api
*/
interface PluginManagerInterface extends DiscoveryInterface, FactoryInterface, MapperInterface {
}

View file

@ -0,0 +1,18 @@
{
"name": "drupal/core-plugin",
"description": "Base building block for a scalable and extensible plug-in system for PHP components and application framework extensions.",
"keywords": ["drupal", "plugin", "plugins"],
"homepage": "https://www.drupal.org/project/drupal",
"license": "GPL-2.0+",
"require": {
"php": ">=5.4.2"
},
"autoload": {
"psr-0": {
"Drupal\\Component\\Plugin\\": ""
}
},
"suggest": {
"symfony/validator": "Leveraged in the use of context aware plugins."
}
}

View file

@ -0,0 +1,294 @@
<?php
/**
* @file
* Contains \Drupal\Component\ProxyBuilder\ProxyBuilder.
*/
namespace Drupal\Component\ProxyBuilder;
/**
* Generates the string representation of the proxy service.
*/
class ProxyBuilder {
/**
* Generates the used proxy class name from a given class name.
*
* @param string $class_name
* The class name of the actual service.
*
* @return string
* The class name of the proxy.
*/
public static function buildProxyClassName($class_name) {
return str_replace('\\', '_', $class_name) . '_Proxy';
}
/**
* Builds a proxy class string.
*
* @param string $class_name
* The class name of the actual service.
*
* @return string
* The full string with namespace class and methods.
*/
public function build($class_name) {
$reflection = new \ReflectionClass($class_name);
$output = '';
$class_documentation = <<<'EOS'
/**
* Provides a proxy class for \{{ class_name }}.
*
* @see \Drupal\Component\ProxyBuilder
*/
EOS;
$class_start = 'class {{ proxy_class_name }}';
// For cases in which the implemented interface is a child of another
// interface, getInterfaceNames() also returns the parent. This causes a
// PHP error.
// In order to avoid that, check for each interface, whether one of its
// parents is also in the list and exclude it.
if ($interfaces = $reflection->getInterfaces()) {
foreach ($interfaces as $interface_name => $interface) {
// Exclude all parents from the list of implemented interfaces of the
// class.
if ($parent_interfaces = $interface->getInterfaceNames()) {
foreach ($parent_interfaces as $parent_interface) {
if (isset($interfaces[$parent_interface])) {}
unset($interfaces[$parent_interface]);
}
}
}
$interface_names = [];
foreach ($interfaces as $interface) {
$interface_names[] = '\\' . $interface->getName();
}
$class_start .= ' implements ' . implode(', ', $interface_names);
}
$output .= $this->buildUseStatements();
// The actual class;
$properties = <<<'EOS'
/**
* @var string
*/
protected $serviceId;
/**
* @var \{{ class_name }}
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
EOS;
$output .= $properties;
// Add all the methods.
$methods = [];
$methods[] = $this->buildConstructorMethod();
$methods[] = $this->buildLazyLoadItselfMethod();
// Add all the methods of the proxied service.
$reflection_methods = $reflection->getMethods();
foreach ($reflection_methods as $method) {
if ($method->getName() === '__construct') {
continue;
}
if ($method->isPublic()) {
$methods[] = $this->buildMethod($method) . "\n";
}
}
$output .= implode("\n", $methods);
// Indent the output.
$output = implode("\n", array_map(function ($value) {
if ($value === '') {
return $value;
}
return " $value";
}, explode("\n", $output)));
$final_output = $class_documentation . $class_start . "\n{\n\n" . $output . "\n}\n";
$final_output = str_replace('{{ class_name }}', $class_name, $final_output);
$final_output = str_replace('{{ proxy_class_name }}', $this->buildProxyClassName($class_name), $final_output);
return $final_output;
}
/**
* Generates the string for the method which loads the actual service.
*
* @return string
*/
protected function buildLazyLoadItselfMethod() {
$output = <<<'EOS'
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$method_name = 'get' . Container::camelize($this->serviceId) . 'Service';
$this->service = $this->container->$method_name(false);
}
return $this->service;
}
EOS;
return $output;
}
/**
* Generates the string representation of a single method: signature, body.
*
* @param \ReflectionMethod $reflection_method
* A reflection method for the method.
*
* @return string
*/
protected function buildMethod(\ReflectionMethod $reflection_method) {
$parameters = [];
foreach ($reflection_method->getParameters() as $parameter) {
$parameters[] = $this->buildParameter($parameter);
}
$function_name = $reflection_method->getName();
$reference = '';
if ($reflection_method->returnsReference()) {
$reference = '&';
}
if ($reflection_method->isStatic()) {
$signature_line = 'public static function ' . $reference . $function_name . '(';
}
else {
$signature_line = 'public function ' . $reference . $function_name . '(';
}
$signature_line .= implode(', ', $parameters);
$signature_line .= ')';
$output = $signature_line . "\n{\n";
$output .= $this->buildMethodBody($reflection_method);
$output .= "\n" . '}';
return $output;
}
/**
* Builds a string for a single parameter of a method.
*
* @param \ReflectionParameter $parameter
* A reflection object of the parameter.
*
* @return string
*/
protected function buildParameter(\ReflectionParameter $parameter) {
$parameter_string = '';
if ($parameter->isArray()) {
$parameter_string .= 'array ';
}
elseif ($parameter->isCallable()) {
$parameter_string .= 'callable ';
}
elseif ($class = $parameter->getClass()) {
$parameter_string .= '\\' . $class->getName() . ' ';
}
if ($parameter->isPassedByReference()) {
$parameter_string .= '&';
}
$parameter_string .= '$' . $parameter->getName();
if ($parameter->isDefaultValueAvailable()) {
$parameter_string .= ' = ';
$parameter_string .= var_export($parameter->getDefaultValue(), TRUE);
}
return $parameter_string;
}
/**
* Builds the body of a wrapped method.
*
* @param \ReflectionMethod $reflection_method
* A reflection method for the method.
*
* @return string
*/
protected function buildMethodBody(\ReflectionMethod $reflection_method) {
$output = '';
$function_name = $reflection_method->getName();
if (!$reflection_method->isStatic()) {
$output .= ' return $this->lazyLoadItself()->' . $function_name . '(';
}
else {
$class_name = $reflection_method->getDeclaringClass()->getName();
$output .= " \\$class_name::$function_name(";
}
// Add parameters;
$parameters = [];
foreach ($reflection_method->getParameters() as $parameter) {
$parameters[] = '$' . $parameter->getName();
}
$output .= implode(', ', $parameters) . ');';
return $output;
}
/**
* Builds the constructor used to inject the actual service ID.
*
* @return string
*/
protected function buildConstructorMethod() {
$output = <<<'EOS'
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $serviceId)
{
$this->container = $container;
$this->serviceId = $serviceId;
}
EOS;
return $output;
}
/**
* Build the required use statements of the proxy class.
*
* @return string
*/
protected function buildUseStatements() {
$output = '';
return $output;
}
}

View file

@ -0,0 +1,78 @@
<?php
/**
* @file
* Contains \Drupal\Component\ProxyBuilder\ProxyDumper.
*/
namespace Drupal\Component\ProxyBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface;
/**
* Dumps the proxy service into the dumped PHP container file.
*/
class ProxyDumper implements DumperInterface {
/**
* Keeps track of already existing proxy classes.
*
* @var array
*/
protected $buildClasses = [];
/**
* The proxy builder.
*
* @var \Drupal\Component\ProxyBuilder\ProxyBuilder
*/
protected $builder;
public function __construct(ProxyBuilder $builder) {
$this->builder = $builder;
}
/**
* {@inheritdoc}
*/
public function isProxyCandidate(Definition $definition) {
return $definition->isLazy() && ($class = $definition->getClass()) && class_exists($class);
}
/**
* {@inheritdoc}
*/
public function getProxyFactoryCode(Definition $definition, $id) {
// Note: the specific get method is called initially with $lazyLoad=TRUE;
// When you want to retrieve the actual service, the code generated in
// ProxyBuilder calls the method with lazy loading disabled.
$output = <<<'EOS'
if ($lazyLoad) {
return $this->services['{{ id }}'] = new {{ class_name }}($this, '{{ id }}');
}
EOS;
$output = str_replace('{{ id }}', $id, $output);
$output = str_replace('{{ class_name }}', $this->builder->buildProxyClassName($definition->getClass()), $output);
return $output;
}
/**
* {@inheritdoc}
*/
public function getProxyCode(Definition $definition) {
// Maybe the same class is used in different services, which are both marked
// as lazy (just think about 2 database connections).
// In those cases we should not generate proxy code the second time.
if (!isset($this->buildClasses[$definition->getClass()])) {
$this->buildClasses[$definition->getClass()] = TRUE;
return $this->builder->build($definition->getClass());
}
else {
return '';
}
}
}

View file

@ -0,0 +1,16 @@
{
"name": "drupal/core-proxy-builder",
"description": "Provides a lightweight mechanism to provide lazy loaded proxies.",
"keywords": ["drupal", "proxy"],
"homepage": "https://www.drupal.org/project/drupal",
"license": "GPL-2.0+",
"require": {
"php": ">=5.4.2",
"symfony/dependency-injection": "~2.6"
},
"autoload": {
"psr-4": {
"Drupal\\Component\\ProxyBuilder\\": ""
}
}
}

View file

@ -0,0 +1,14 @@
Drupal Components are independent libraries that do not depend on the rest of
Drupal in order to function.
Components MAY depend on other Drupal Components or external libraries/packages,
but MUST NOT depend on any other Drupal code.
In other words, only dependencies that can be specified in a composer.json file
of the Component are acceptable dependencies. Every Drupal Component presents a
valid dependency, because it is assumed to contain a composer.json file (even
if it may not exist yet).
Each Component should be in its own namespace, and should be as self-contained
as possible. It should be possible to split a Component off to its own
repository and use as a stand-alone library, independently of Drupal.

View file

@ -0,0 +1,14 @@
<?php
/**
* @file
* Contains \Drupal\Component\Serialization\Exception\InvalidDataTypeException.
*/
namespace Drupal\Component\Serialization\Exception;
/**
* Exception thrown when a data type is invalid.
*/
class InvalidDataTypeException extends \InvalidArgumentException {
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\Component\Serialization\Json.
*/
namespace Drupal\Component\Serialization;
/**
* Default serialization for JSON.
*
* @ingroup third_party
*/
class Json implements SerializationInterface {
/**
* {@inheritdoc}
*
* Uses HTML-safe strings, with several characters escaped.
*/
public static function encode($variable) {
// Encode <, >, ', &, and ".
return json_encode($variable, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
}
/**
* {@inheritdoc}
*/
public static function decode($string) {
return json_decode($string, TRUE);
}
/**
* {@inheritdoc}
*/
public static function getFileExtension() {
return 'json';
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\Component\Serialization\PhpSerialize.
*/
namespace Drupal\Component\Serialization;
/**
* Default serialization for serialized PHP.
*/
class PhpSerialize implements SerializationInterface {
/**
* {@inheritdoc}
*/
public static function encode($data) {
return serialize($data);
}
/**
* {@inheritdoc}
*/
public static function decode($raw) {
return unserialize($raw);
}
/**
* {@inheritdoc}
*/
public static function getFileExtension() {
return 'serialized';
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\Component\Serialization\SerializationInterface.
*/
namespace Drupal\Component\Serialization;
/**
* Defines an interface for serialization formats.
*/
interface SerializationInterface {
/**
* Encodes data into the serialization format.
*
* @param mixed $data
* The data to encode.
*
* @return string
* The encoded data.
*/
public static function encode($data);
/**
* Decodes data from the serialization format.
*
* @param string $raw
* The raw data string to decode.
*
* @return mixed
* The decoded data.
*/
public static function decode($raw);
/**
* Gets the file extension for this serialization format.
*
* @return string
* The file extension, without leading dot.
*/
public static function getFileExtension();
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\Component\Serialization\Yaml.
*/
namespace Drupal\Component\Serialization;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Dumper;
/**
* Default serialization for YAML using the Symfony component.
*/
class Yaml implements SerializationInterface {
/**
* {@inheritdoc}
*/
public static function encode($data) {
try {
$yaml = new Dumper();
$yaml->setIndentation(2);
return $yaml->dump($data, PHP_INT_MAX, 0, TRUE, FALSE);
}
catch (\Exception $e) {
throw new InvalidDataTypeException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public static function decode($raw) {
try {
$yaml = new Parser();
// Make sure we have a single trailing newline. A very simple config like
// 'foo: bar' with no newline will fail to parse otherwise.
return $yaml->parse($raw, TRUE, FALSE);
}
catch (\Exception $e) {
throw new InvalidDataTypeException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public static function getFileExtension() {
return 'yml';
}
}

View file

@ -0,0 +1,288 @@
<?php
/**
* @file
* Contains \Drupal\Component\Transliteration\PhpTransliteration.
*
* Some parts of this code were derived from the MediaWiki project's UtfNormal
* class, Copyright © 2004 Brion Vibber <brion@pobox.com>,
* http://www.mediawiki.org/
*/
namespace Drupal\Component\Transliteration;
/**
* Implements transliteration without using the PECL extensions.
*
* Transliterations are done character-by-character, by looking up non-US-ASCII
* characters in a transliteration database.
*
* The database comes from two types of files, both of which are searched for in
* the PhpTransliteration::$dataDirectory directory. First, language-specific
* overrides are searched (see PhpTransliteration::readLanguageOverrides()). If
* there is no language-specific override for a character, the generic
* transliteration character tables are searched (see
* PhpTransliteration::readGenericData()). If looking up the character in the
* generic table results in a NULL value, or an illegal character is
* encountered, then a substitute character is returned.
*/
class PhpTransliteration implements TransliterationInterface {
/**
* Directory where data for transliteration resides.
*
* The constructor sets this (by default) to subdirectory 'data' underneath
* the directory where the class's PHP file resides.
*
* @var string
*/
protected $dataDirectory;
/**
* Associative array of language-specific character transliteration tables.
*
* The outermost array keys are language codes. For each language code key,
* the value is an array whose keys are Unicode character codes, and whose
* values are the transliterations of those characters to US-ASCII. This is
* set up as needed in PhpTransliteration::replace() by calling
* PhpTransliteration::readLanguageOverrides().
*
* @var array
*/
protected $languageOverrides = array();
/**
* Non-language-specific transliteration tables.
*
* Array whose keys are the upper two bytes of the Unicode character, and
* whose values are an array of transliterations for each lower-two bytes
* character code. This is set up as needed in PhpTransliteration::replace()
* by calling PhpTransliteration::readGenericData().
*
* @var array
*/
protected $genericMap = array();
/**
* Constructs a transliteration object.
*
* @param string $data_directory
* (optional) The directory where data files reside. If omitted, defaults
* to subdirectory 'data' underneath the directory where the class's PHP
* file resides.
*/
public function __construct($data_directory = NULL) {
$this->dataDirectory = (isset($data_directory)) ? $data_directory : __DIR__ . '/data';
}
/**
* {@inheritdoc}
*/
public function removeDiacritics($string) {
$result = '';
foreach (preg_split('//u', $string, 0, PREG_SPLIT_NO_EMPTY) as $character) {
$code = self::ordUTF8($character);
// These two Unicode ranges include the accented US-ASCII letters, with a
// few characters that aren't accented letters mixed in. So define the
// ranges and the excluded characters.
$range1 = $code > 0x00bf && $code < 0x017f;
$exclusions_range1 = array(0x00d0, 0x00d7, 0x00f0, 0x00f7, 0x0138, 0x014a, 0x014b);
$range2 = $code > 0x01cc && $code < 0x0250;
$exclusions_range2 = array(0x01DD, 0x01f7, 0x021c, 0x021d, 0x0220, 0x0221, 0x0241, 0x0242, 0x0245);
$replacement = $character;
if (($range1 && !in_array($code, $exclusions_range1)) || ($range2 && !in_array($code, $exclusions_range2))) {
$to_add = $this->lookupReplacement($code, 'xyz');
if(strlen($to_add) === 1) {
$replacement = $to_add;
}
}
$result .= $replacement;
}
return $result;
}
/**
* {@inheritdoc}
*/
public function transliterate($string, $langcode = 'en', $unknown_character = '?', $max_length = NULL) {
$result = '';
$length = 0;
// Split into Unicode characters and transliterate each one.
foreach (preg_split('//u', $string, 0, PREG_SPLIT_NO_EMPTY) as $character) {
$code = self::ordUTF8($character);
if ($code == -1) {
$to_add = $unknown_character;
}
else {
$to_add = $this->replace($code, $langcode, $unknown_character);
}
// Check if this exceeds the maximum allowed length.
if (isset($max_length)) {
$length += strlen($to_add);
if ($length > $max_length) {
// There is no more space.
return $result;
}
}
$result .= $to_add;
}
return $result;
}
/**
* Finds the character code for a UTF-8 character: like ord() but for UTF-8.
*
* @param string $character
* A single UTF-8 character.
*
* @return int
* The character code, or -1 if an illegal character is found.
*/
protected static function ordUTF8($character) {
$first_byte = ord($character[0]);
if (($first_byte & 0x80) == 0) {
// Single-byte form: 0xxxxxxxx.
return $first_byte;
}
if (($first_byte & 0xe0) == 0xc0) {
// Two-byte form: 110xxxxx 10xxxxxx.
return (($first_byte & 0x1f) << 6) + (ord($character[1]) & 0x3f);
}
if (($first_byte & 0xf0) == 0xe0) {
// Three-byte form: 1110xxxx 10xxxxxx 10xxxxxx.
return (($first_byte & 0x0f) << 12) + ((ord($character[1]) & 0x3f) << 6) + (ord($character[2]) & 0x3f);
}
if (($first_byte & 0xf8) == 0xf0) {
// Four-byte form: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx.
return (($first_byte & 0x07) << 18) + ((ord($character[1]) & 0x3f) << 12) + ((ord($character[2]) & 0x3f) << 6) + (ord($character[3]) & 0x3f);
}
// Other forms are not legal.
return -1;
}
/**
* Replaces a single Unicode character using the transliteration database.
*
* @param int $code
* The character code of a Unicode character.
* @param string $langcode
* The language code of the language the character is in.
* @param string $unknown_character
* The character to substitute for characters without transliterated
* equivalents.
*
* @return string
* US-ASCII replacement character. If it has a mapping, it is returned;
* otherwise, $unknown_character is returned. The replacement can contain
* multiple characters.
*/
protected function replace($code, $langcode, $unknown_character) {
if ($code < 0x80) {
// Already lower ASCII.
return chr($code);
}
// See if there is a language-specific override for this character.
if (!isset($this->languageOverrides[$langcode])) {
$this->readLanguageOverrides($langcode);
}
if (isset($this->languageOverrides[$langcode][$code])) {
return $this->languageOverrides[$langcode][$code];
}
return $this->lookupReplacement($code, $unknown_character);
}
/**
* Look up the generic replacement for a UTF-8 character code.
*
* @param $code
* The UTF-8 character code.
* @param string $unknown_character
* (optional) The character to substitute for characters without entries in
* the replacement tables.
*
* @return string
* US-ASCII replacement characters. If it has a mapping, it is returned;
* otherwise, $unknown_character is returned. The replacement can contain
* multiple characters.
*/
protected function lookupReplacement($code, $unknown_character = '?') {
// See if there is a generic mapping for this character.
$bank = $code >> 8;
if (!isset($this->genericMap[$bank])) {
$this->readGenericData($bank);
}
$code = $code & 0xff;
return isset($this->genericMap[$bank][$code]) ? $this->genericMap[$bank][$code] : $unknown_character;
}
/**
* Reads in language overrides for a language code.
*
* The data is read from files named "$langcode.php" in
* PhpTransliteration::$dataDirectory. These files should set up an array
* variable $overrides with an element whose key is $langcode and whose value
* is an array whose keys are character codes, and whose values are their
* transliterations in this language. The character codes can be for any valid
* Unicode character, independent of the number of bytes.
*
* @param $langcode
* Code for the language to read.
*/
protected function readLanguageOverrides($langcode) {
// Figure out the file name to use by sanitizing the language code,
// just in case.
$file = $this->dataDirectory . '/' . preg_replace('/[^a-zA-Z\-]/', '', $langcode) . '.php';
// Read in this file, which should set up a variable called $overrides,
// which will be local to this function.
if (is_file($file)) {
include $file;
}
if (!isset($overrides) || !is_array($overrides)) {
$overrides = array($langcode => array());
}
$this->languageOverrides[$langcode] = $overrides[$langcode];
}
/**
* Reads in generic transliteration data for a bank of characters.
*
* The data is read in from a file named "x$bank.php" (with $bank in
* hexadecimal notation) in PhpTransliteration::$dataDirectory. These files
* should set up a variable $bank containing an array whose numerical indices
* are the remaining two bytes of the character code, and whose values are the
* transliterations of these characters into US-ASCII. Note that the maximum
* Unicode character that can be encoded in this way is 4 bytes.
*
* @param $bank
* First two bytes of the Unicode character, or 0 for the ASCII range.
*/
protected function readGenericData($bank) {
// Figure out the file name.
$file = $this->dataDirectory . '/x' . sprintf('%02x', $bank) . '.php';
// Read in this file, which should set up a variable called $base, which
// will be local to this function.
if (is_file($file)) {
include $file;
}
if (!isset($base) || !is_array($base)) {
$base = array();
}
// Save this data.
$this->genericMap[$bank] = $base;
}
}

View file

@ -0,0 +1,54 @@
<?php
/**
* @file
* Contains \Drupal\Component\Transliteration\TransliterationInterface.
*/
namespace Drupal\Component\Transliteration;
/**
* Defines an interface for classes providing transliteration.
*
* @ingroup transliteration
*/
interface TransliterationInterface {
/**
* Removes diacritics (accents) from certain letters.
*
* This only applies to certain letters: Accented Latin characters like
* a-with-acute-accent, in the UTF-8 character range of 0xE0 to 0xE6 and
* 01CD to 024F. Replacements that would result in the string changing length
* are excluded, as well as characters that are not accented US-ASCII letters.
*
* @param string $string
* The string holding diacritics.
*
* @return string
* $string with accented letters replaced by their unaccented equivalents.
*/
public function removeDiacritics($string);
/**
* Transliterates text from Unicode to US-ASCII.
*
* @param string $string
* The string to transliterate.
* @param string $langcode
* (optional) The language code of the language the string is in. Defaults
* to 'en' if not provided. Warning: this can be unfiltered user input.
* @param string $unknown_character
* (optional) The character to substitute for characters in $string without
* transliterated equivalents. Defaults to '?'.
* @param int $max_length
* (optional) If provided, return at most this many characters, ensuring
* that the transliteration does not split in the middle of an input
* character's transliteration.
*
* @return string
* $string with non-US-ASCII characters transliterated to US-ASCII
* characters, and unknown characters replaced with $unknown_character.
*/
public function transliterate($string, $langcode = 'en', $unknown_character = '?', $max_length = NULL);
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* German transliteration data for the PhpTransliteration class.
*/
$overrides['de'] = array(
0xC4 => 'Ae',
0xD6 => 'Oe',
0xDC => 'Ue',
0xE4 => 'ae',
0xF6 => 'oe',
0xFC => 'ue',
);

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Danish transliteration data for the PhpTransliteration class.
*/
$overrides['dk'] = array(
0xC5 => 'Aa',
0xD8 => 'Oe',
0xE5 => 'aa',
0xF8 => 'oe',
);

View file

@ -0,0 +1,21 @@
<?php
/**
* @file
* Esperanto transliteration data for the PhpTransliteration class.
*/
$overrides['eo'] = array(
0x18 => 'Cx',
0x19 => 'cx',
0x11C => 'Gx',
0x11D => 'gx',
0x124 => 'Hx',
0x125 => 'hx',
0x134 => 'Jx',
0x135 => 'jx',
0x15C => 'Sx',
0x15D => 'sx',
0x16C => 'Ux',
0x16D => 'ux',
);

View file

@ -0,0 +1,31 @@
<?php
/**
* @file
* Kyrgyz transliteration data for the PhpTransliteration class.
*/
$overrides['kg'] = array(
0x41 => 'E',
0x416 => 'C',
0x419 => 'J',
0x425 => 'X',
0x426 => 'TS',
0x429 => 'SCH',
0x42E => 'JU',
0x42F => 'JA',
0x436 => 'c',
0x439 => 'j',
0x445 => 'x',
0x446 => 'ts',
0x449 => 'sch',
0x44E => 'ju',
0x44F => 'ja',
0x451 => 'e',
0x4A2 => 'H',
0x4A3 => 'h',
0x4AE => 'W',
0x4AF => 'w',
0x4E8 => 'Q',
0x4E9 => 'q',
);

View file

@ -0,0 +1,18 @@
<?php
/**
* @file
* Generic transliteration data for the PhpTransliteration class.
*/
$base = array(
// Note: to save memory plain ASCII mappings have been left out.
0x80 => '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
0x90 => '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
0xA0 => ' ', '!', 'C/', 'PS', '$?', 'Y=', '|', 'SS', '"', '(C)', 'a', '<<', '!', '-', '(R)', '-',
0xB0 => 'deg', '+-', '2', '3', '\'', 'm', 'P', ':', ',', '1', 'o', '>>', ' 1/4', ' 1/2', ' 3/4', '?',
0xC0 => 'A', 'A', 'A', 'A', 'A', 'A', 'AE', 'C', 'E', 'E', 'E', 'E', 'I', 'I', 'I', 'I',
0xD0 => 'D', 'N', 'O', 'O', 'O', 'O', 'O', '*', 'O', 'U', 'U', 'U', 'U', 'Y', 'TH', 'ss',
0xE0 => 'a', 'a', 'a', 'a', 'a', 'a', 'ae', 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i',
0xF0 => 'd', 'n', 'o', 'o', 'o', 'o', 'o', '/', 'o', 'u', 'u', 'u', 'u', 'y', 'th', 'y',
);

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Generic transliteration data for the PhpTransliteration class.
*/
$base = array(
0x00 => 'A', 'a', 'A', 'a', 'A', 'a', 'C', 'c', 'C', 'c', 'C', 'c', 'C', 'c', 'D', 'd',
0x10 => 'D', 'd', 'E', 'e', 'E', 'e', 'E', 'e', 'E', 'e', 'E', 'e', 'G', 'g', 'G', 'g',
0x20 => 'G', 'g', 'G', 'g', 'H', 'h', 'H', 'h', 'I', 'i', 'I', 'i', 'I', 'i', 'I', 'i',
0x30 => 'I', 'i', 'IJ', 'ij', 'J', 'j', 'K', 'k', 'q', 'L', 'l', 'L', 'l', 'L', 'l', 'L',
0x40 => 'l', 'L', 'l', 'N', 'n', 'N', 'n', 'N', 'n', '\'n', 'N', 'n', 'O', 'o', 'O', 'o',
0x50 => 'O', 'o', 'OE', 'oe', 'R', 'r', 'R', 'r', 'R', 'r', 'S', 's', 'S', 's', 'S', 's',
0x60 => 'S', 's', 'T', 't', 'T', 't', 'T', 't', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 'u',
0x70 => 'U', 'u', 'U', 'u', 'W', 'w', 'Y', 'y', 'Y', 'Z', 'z', 'Z', 'z', 'Z', 'z', 's',
0x80 => 'b', 'B', 'B', 'b', '6', '6', 'O', 'C', 'c', 'D', 'D', 'D', 'd', 'd', '3', '@',
0x90 => 'E', 'F', 'f', 'G', 'G', 'hv', 'I', 'I', 'K', 'k', 'l', 'l', 'W', 'N', 'n', 'O',
0xA0 => 'O', 'o', 'OI', 'oi', 'P', 'p', 'YR', '2', '2', 'SH', 'sh', 't', 'T', 't', 'T', 'U',
0xB0 => 'u', 'Y', 'V', 'Y', 'y', 'Z', 'z', 'ZH', 'ZH', 'zh', 'zh', '2', '5', '5', 'ts', 'w',
0xC0 => '|', '||', '|=', '!', 'DZ', 'Dz', 'dz', 'LJ', 'Lj', 'lj', 'NJ', 'Nj', 'nj', 'A', 'a', 'I',
0xD0 => 'i', 'O', 'o', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 'u', '@', 'A', 'a',
0xE0 => 'A', 'a', 'AE', 'ae', 'G', 'g', 'G', 'g', 'K', 'k', 'O', 'o', 'O', 'o', 'ZH', 'zh',
0xF0 => 'j', 'DZ', 'Dz', 'dz', 'G', 'g', 'HV', 'W', 'N', 'n', 'A', 'a', 'AE', 'ae', 'O', 'o',
);

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Generic transliteration data for the PhpTransliteration class.
*/
$base = array(
0x00 => 'A', 'a', 'A', 'a', 'E', 'e', 'E', 'e', 'I', 'i', 'I', 'i', 'O', 'o', 'O', 'o',
0x10 => 'R', 'r', 'R', 'r', 'U', 'u', 'U', 'u', 'S', 's', 'T', 't', 'Y', 'y', 'H', 'h',
0x20 => 'N', 'd', 'OU', 'ou', 'Z', 'z', 'A', 'a', 'E', 'e', 'O', 'o', 'O', 'o', 'O', 'o',
0x30 => 'O', 'o', 'Y', 'y', 'l', 'n', 't', 'j', 'db', 'qp', 'A', 'C', 'c', 'L', 'T', 's',
0x40 => 'z', '?', '?', 'B', 'U', 'V', 'E', 'e', 'J', 'j', 'Q', 'q', 'R', 'r', 'Y', 'y',
0x50 => 'a', 'a', 'a', 'b', 'o', 'c', 'd', 'd', 'e', '@', '@', 'e', 'e', 'e', 'e', 'j',
0x60 => 'g', 'g', 'G', 'g', 'u', 'Y', 'h', 'h', 'i', 'i', 'I', 'l', 'l', 'l', 'lZ', 'W',
0x70 => 'W', 'm', 'n', 'n', 'N', 'o', 'OE', 'O', 'F', 'R', 'R', 'R', 'r', 'r', 'r', 'R',
0x80 => 'R', 'R', 's', 'S', 'j', 'S', 'S', 't', 't', 'u', 'U', 'v', '^', 'W', 'Y', 'Y',
0x90 => 'z', 'z', 'Z', 'Z', '?', '?', '?', 'C', '@', 'B', 'E', 'G', 'H', 'j', 'k', 'L',
0xA0 => 'q', '?', '?', 'dz', 'dZ', 'dz', 'ts', 'tS', 'tC', 'fN', 'ls', 'lz', 'WW', ']]', 'h', 'h',
0xB0 => 'k', 'h', 'j', 'r', 'r', 'r', 'r', 'w', 'y', '\'', '"', '`', '\'', '`', '`', '\'',
0xC0 => '?', '?', '<', '>', '^', 'V', '^', 'V', '\'', '-', '/', '\\', ',', '_', '\\', '/',
0xD0 => ':', '.', '`', '\'', '^', 'V', '+', '-', 'V', '.', '@', ',', '~', '"', 'R', 'X',
0xE0 => 'G', 'l', 's', 'x', '?', '', '', '', '', '', '', '', 'V', '=', '"', NULL,
0xF0 => NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
);

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Generic transliteration data for the PhpTransliteration class.
*/
$base = array(
0x00 => '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
0x10 => '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
0x20 => '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
0x30 => '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
0x40 => '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', NULL,
0x50 => NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
0x60 => '', '', '', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
0x70 => NULL, NULL, NULL, NULL, '\'', ',', NULL, NULL, NULL, NULL, 'i', NULL, NULL, NULL, '?', NULL,
0x80 => NULL, NULL, NULL, NULL, '', '', 'A', ':', 'E', 'E', 'I', NULL, 'O', NULL, 'Y', 'O',
0x90 => 'i', 'A', 'B', 'G', 'D', 'E', 'Z', 'E', 'TH', 'I', 'K', 'L', 'M', 'N', 'X', 'O',
0xA0 => 'P', 'R', NULL, 'S', 'T', 'Y', 'PH', 'CH', 'PS', 'O', 'I', 'Y', 'a', 'e', 'e', 'i',
0xB0 => 'y', 'a', 'b', 'g', 'd', 'e', 'z', 'e', 'th', 'i', 'k', 'l', 'm', 'n', 'x', 'o',
0xC0 => 'p', 'r', 's', 's', 't', 'y', 'ph', 'ch', 'ps', 'o', 'i', 'y', 'o', 'y', 'o', NULL,
0xD0 => 'b', 'th', 'Y', 'Y', 'Y', 'ph', 'p', '&', NULL, NULL, 'St', 'st', 'W', 'w', 'Q', 'q',
0xE0 => 'Sp', 'sp', 'Sh', 'sh', 'F', 'f', 'Kh', 'kh', 'H', 'h', 'G', 'g', 'CH', 'ch', 'Ti', 'ti',
0xF0 => 'k', 'r', 's', 'j', 'TH', 'e', NULL, 'S', 's', 'S', 'S', 's', NULL, NULL, NULL, NULL,
);

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Generic transliteration data for the PhpTransliteration class.
*/
$base = array(
0x00 => 'E', 'E', 'D', 'G', 'E', 'Z', 'I', 'I', 'J', 'L', 'N', 'C', 'K', 'I', 'U', 'D',
0x10 => 'A', 'B', 'V', 'G', 'D', 'E', 'Z', 'Z', 'I', 'I', 'K', 'L', 'M', 'N', 'O', 'P',
0x20 => 'R', 'S', 'T', 'U', 'F', 'H', 'C', 'C', 'S', 'S', '', 'Y', '', 'E', 'U', 'A',
0x30 => 'a', 'b', 'v', 'g', 'd', 'e', 'z', 'z', 'i', 'i', 'k', 'l', 'm', 'n', 'o', 'p',
0x40 => 'r', 's', 't', 'u', 'f', 'h', 'c', 'c', 's', 's', '', 'y', '', 'e', 'u', 'a',
0x50 => 'e', 'e', 'd', 'g', 'e', 'z', 'i', 'i', 'j', 'l', 'n', 'c', 'k', 'i', 'u', 'd',
0x60 => 'O', 'o', 'E', 'e', 'Ie', 'ie', 'E', 'e', 'Ie', 'ie', 'O', 'o', 'Io', 'io', 'Ks', 'ks',
0x70 => 'Ps', 'ps', 'F', 'f', 'Y', 'y', 'Y', 'y', 'u', 'u', 'O', 'o', 'O', 'o', 'Ot', 'ot',
0x80 => 'Q', 'q', '*1000*', '', '', '', '', NULL, '*100.000*', '*1.000.000*', NULL, NULL, '"', '"', 'R\'', 'r\'',
0x90 => 'G', 'g', 'G', 'g', 'G', 'g', 'Zh\'', 'zh\'', 'Z', 'z', 'K\'', 'k\'', 'K\'', 'k\'', 'K\'', 'k\'',
0xA0 => 'K\'', 'k\'', 'N\'', 'n\'', 'Ng', 'ng', 'P\'', 'p\'', 'Kh', 'kh', 'S\'', 's\'', 'T\'', 't\'', 'U', 'u',
0xB0 => 'U\'', 'u\'', 'Kh\'', 'kh\'', 'Tts', 'tts', 'Ch\'', 'ch\'', 'Ch\'', 'ch\'', 'H', 'h', 'Ch', 'ch', 'Ch\'', 'ch\'',
0xC0 => '`', 'Z', 'z', 'K\'', 'k\'', NULL, NULL, 'N\'', 'n\'', NULL, NULL, 'Ch', 'ch', NULL, NULL, NULL,
0xD0 => 'A', 'a', 'A', 'a', 'AE', 'ae', 'E', 'e', '@', '@', '@', '@', 'Z', 'z', 'Z', 'z',
0xE0 => 'Dz', 'dz', 'I', 'i', 'I', 'i', 'O', 'o', 'O', 'o', 'O', 'o', 'E', 'e', 'U', 'u',
0xF0 => 'U', 'u', 'U', 'u', 'C', 'c', NULL, NULL, 'Y', 'y', NULL, NULL, NULL, NULL, NULL, NULL,
);

Some files were not shown because too many files have changed in this diff Show more