diff --git a/CHANGELOG-3.4.md b/CHANGELOG-3.4.md new file mode 100644 index 0000000000..2790538d54 --- /dev/null +++ b/CHANGELOG-3.4.md @@ -0,0 +1,217 @@ +CHANGELOG for 3.4.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 3.4 minor versions. + +To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash +To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v3.4.0...v3.4.1 + +* 3.4.0-BETA1 (2017-10-18) + + * feature #24583 Adding a new debug:autowiring command (weaverryan) + * feature #24523 [HttpFoundation] Make sessions secure and lazy (nicolas-grekas) + * feature #22610 [Form] [TwigBridge] Added option to disable usage of default themes when rendering a form (emodric) + * feature #23112 [OptionsResolver] Support array of types in allowed type (pierredup) + * feature #24321 added ability to handle parent classes for PropertyNormalizer (ivoba) + * feature #24505 [HttpKernel] implement reset() in DumpDataCollector (xabbuh) + * feature #24425 [Console][HttpKernel] Handle new SHELL_VERBOSITY env var, also configures the default logger (nicolas-grekas) + * feature #24387 [FORM] Prevent forms from extending itself as a parent (pierredup) + * feature #24484 [DI] Throw accurate failures when accessing removed services (nicolas-grekas) + * feature #24208 [Form] Display option definition from a given form type (yceruto, ogizanagi) + * feature #23499 [Workflow] add guard is_valid() method support (alain-flaus, lyrixx) + * feature #24388 [Security] Look at headers for switch_user username (chalasr) + * feature #23708 Added deprecation to cwd not existing Fixes #18249 (alexbowers) + * feature #24443 [Session] deprecate MemcacheSessionHandler (Tobion) + * feature #24409 [Bridge\Doctrine][FrameworkBundle] Deprecate some remaining uses of ContainerAwareTrait (nicolas-grekas) + * feature #24438 [Session][VarDumper] Deprecate accepting legacy mongo extension (Tobion) + * feature #24389 [DoctrineBridge] Deprecate dbal session handler (Tobion) + * feature #16835 [Security] Add Guard authenticator method (Amo, chalasr) + * feature #24289 [FrameworkBundle][HttpKernel] Reset profiler (derrabus) + * feature #24144 [FrameworkBundle] Expose dotenv in bin/console about (ro0NL) + * feature #24403 [FrameworkBundle][Routing] Show welcome message if no routes are configured (yceruto) + * feature #22679 [Form] Add tel and color types (apetitpa) + * feature #23845 [Validator] Add unique entity violation cause (Ilya Vertakov) + * feature #22132 [Lock] Automaticaly release lock when user forget it (jderusse) + * feature #21751 Bootstrap4 support for Twig form theme (hiddewie, javiereguiluz) + * feature #24383 [FrameworkBundle] Don't clear app pools on cache:clear (nicolas-grekas) + * feature #24148 [Form] Hide label button when its setted to false (TeLiXj) + * feature #24378 [SecurityBundle] Deprecate auto picking the first provider (ogizanagi) + * feature #24260 [Security] Add impersonation support for stateless authentication (chalasr) + * feature #24300 [HttpKernel][FrameworkBundle] Add a minimalist default PSR-3 logger (dunglas) + * feature #21604 [Security] Argon2i Password Encoder (zanbaldwin) + * feature #24372 [DowCrawler] Default to UTF-8 when possible (nicolas-grekas) + * feature #24264 [TwigBundle] Improve the overriding of bundle templates (yceruto) + * feature #24197 [Translation] Moved PhpExtractor and PhpStringTokenParser to Translation component (Nyholm) + * feature #24362 [HttpKernel] Deprecate some compiler passes in favor of tagged iterator args (nicolas-grekas) + * feature #21027 [Asset] Provide default context (ro0NL) + * feature #22200 [DI] Reference tagged services in config (ro0NL) + * feature #24337 Adding a shortcuts for the main security functionality (weaverryan, javiereguiluz) + * feature #24358 [TwigBundle] register an identity translator as fallback (xabbuh) + * feature #24357 [Yaml] include file and line no in deprecation message (xabbuh) + * feature #24330 [FrameworkBundle] register class metadata factory alias (xabbuh) + * feature #24349 [SecurityBundle] Add missing AclSchemaListener deprecation (ogizanagi) + * feature #24202 [Filesystem] deprecate relative paths in makePathRelative() (xabbuh) + * feature #21716 [Serializer] Add Support for `object_to_populate` in CustomNormalizer (chrisguitarguy) + * feature #21960 Remove Validator\TypeTestCase and add validator logic to base TypeTestCase (pierredup) + * feature #22113 [Lock] Include lock component in framework bundle (jderusse) + * feature #24236 [WebProfilerBundle] Render file links for twig templates (ro0NL) + * feature #21239 [Serializer] throw more specific exceptions (xabbuh) + * feature #24256 CsvEncoder handling variable structures and custom header order (Oliver Hoff) + * feature #23471 [Finder] Add a method to check if any results were found (duncan3dc) + * feature #23149 [PhpUnitBridge] Added a CoverageListener to enhance the code coverage report (lyrixx) + * feature #24318 [SecurityBundle] Deprecate ACL related code (chalasr) + * feature #24335 [Security][SecurityBundle] Deprecate the HTTP digest auth (ogizanagi) + * feature #21951 [Security][Firewall] Passing the newly generated security token to the event during user switching (klandaika) + * feature #23485 [Config] extracted the xml parsing from XmlUtils::loadFile into XmlUtils::parse (Basster) + * feature #22890 [HttpKernel] Add ability to configure catching exceptions for Client (kbond) + * feature #24239 [HttpFoundation] Deprecate compatibility with PHP <5.4 sessions (afurculita) + * feature #23882 [Security] Deprecated not being logged out after user change (iltar) + * feature #24200 Added an alias for FlashBagInterface in config (tifabien) + * feature #24295 [DI][DX] Throw exception on some ContainerBuilder methods used from extensions (ogizanagi) + * feature #24253 [Yaml] support parsing files (xabbuh) + * feature #24290 Adding Definition::addError() and a compiler pass to throw errors as exceptions (weaverryan) + * feature #24301 [DI] Add AutowireRequiredMethodsPass to fix bindings for `@required` methods (nicolas-grekas) + * feature #24226 [Cache] Add ResettableInterface to allow resetting any pool's local state (nicolas-grekas) + * feature #24303 [FrameworkBundle] allow forms without translations and validator (xabbuh) + * feature #24291 [SecurityBundle] Reset the authentication token between requests (derrabus) + * feature #24280 [VarDumper] Make `dump()` a little bit more easier to use (freekmurze) + * feature #24277 [Serializer] Getter for extra attributes in ExtraAttributesException (mdeboer) + * feature #24257 [HttpKernel][DI] Enable Kernel to implement CompilerPassInterface (nicolas-grekas) + * feature #23834 [DI] Add "PHP fluent format" for configuring the container (nicolas-grekas) + * feature #24180 [Routing] Add PHP fluent DSL for configuring routes (nicolas-grekas) + * feature #24232 [Bridge\Doctrine] Add "DoctrineType::reset()" method (nicolas-grekas) + * feature #24238 [DI] Turn services and aliases private by default, with BC layer (nicolas-grekas) + * feature #23648 [Form] Add input + regions options to TimezoneType (ro0NL) + * feature #24185 [Form] Display general forms information on debug:form (yceruto) + * feature #23747 [Serializer][FrameworkBundle] Add a DateInterval normalizer (Lctrs) + * feature #24193 [FrameworkBundle] Reset stopwatch between requests (derrabus) + * feature #24160 [HttpKernel] Deprecate bundle inheritance (fabpot) + * feature #24155 [FrameworkBundle][HttpKernel] Add DI tag for resettable services (derrabus) + * feature #23625 Feature #23583 Add current and fallback locales in WDT / Profiler (nemoneph) + * feature #24179 [TwigBundle] Add default templates directory and option to configure it (yceruto) + * feature #24104 Make as many services private as possible (nicolas-grekas) + * feature #18314 [Translation] added support for adding custom message formatter (aitboudad) + * feature #24158 deprecated profiler.matcher configuration (fabpot) + * feature #24131 [Console] Do not display short exception trace for common console exceptions (yceruto) + * feature #24080 Deprecated the web_profiler.position option (javiereguiluz) + * feature #24114 [SecurityBundle] Throw a meaningful exception when an undefined user provider is used inside a firewall (chalasr) + * feature #24122 [DI] rename ResolveDefinitionTemplatesPass to ResolveChildDefinitionsPass (nicolas-grekas) + * feature #23901 [DI] Allow processing env vars (nicolas-grekas) + * feature #24093 [FrameworkBundle] be able to enable workflow support explicitly (xabbuh) + * feature #24064 [TwigBridge] Show Twig's loader paths on debug:twig command (yceruto) + * feature #23978 [Cache] Use options from Memcached DSN (Bukashk0zzz) + * feature #24075 Implemented PruneableInterface on TagAwareAdapter (Toflar) + * feature #21414 [Console] Display file and line on Exception (arno14) + * feature #24068 [HttpKernel] Deprecate EnvParametersResource (ro0NL) + * feature #22542 [Lock] Check TTL expiration in lock acquisition (jderusse) + * feature #24031 [Routing] Add the possibility to define a prefix for all routes of a controller (fabpot) + * feature #23967 [VarDumper] add force-collapse/expand + use it for traces (nicolas-grekas) + * feature #24033 [DI] Add ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE (nicolas-grekas) + * feature #24026 [Security] add impersonator_user to "User was reloaded" log message (gharlan) + * feature #23603 [Cache] Add (pdo|chain) cache (adapter|simple) prune method (robfrawley) + * feature #23694 [Form] Add debug:form command (yceruto) + * feature #24028 [Yaml] mark some classes as final (xabbuh) + * feature #22543 [Lock] Expose an expiringDate and isExpired method in Lock (jderusse) + * feature #23667 [Translation] Create an TranslationReaderInterface and move TranslationLoader to TranslationComponent (Nyholm) + * feature #24024 [config] Add ability to deprecate a node (sanpii) + * feature #23668 [VarDumper] Add period caster (maidmaid) + * feature #23991 [DI] Improve psr4-based service discovery (alternative implementation) (kbond) + * feature #23947 [Translation] Adding the ability do load in xliff2.0 (Nyholm) + * feature #23887 [Console] Allow commands to provide a default name for compile time registration (chalasr, nicolas-grekas) + * feature #23874 [DI] Case sensitive parameter names (ro0NL) + * feature #23936 Remove some sf2 references (fabpot) + * feature #23680 [Webprofiler] Added blocks that allows extension of the profiler page. (Nyholm) + * feature #23665 Create an interface for TranslationWriter (Nyholm) + * feature #23890 [Translation] Adding the ability do dump in xliff2.0 (Nyholm) + * feature #23862 [SecurityBundle] resolve class name parameter inside AddSecurityVotersPass (pjarmalavicius) + * feature #23915 [DI] Allow get available services from service locator (Koc) + * feature #23792 [HttpKernel][FrameworkBundle] Add RebootableInterface, fix and un-deprecate cache:clear with warmup (nicolas-grekas) + * feature #23227 Add support for "controller" keyword for configuring routes controllers (voronkovich) + * feature #23869 [Console] Made console command shortcuts case insensitive (thanosp) + * feature #23855 [DI] Allow dumping inline services in Yaml (nicolas-grekas) + * feature #23836 [FrameworkBundle] Catch Fatal errors in commands registration (chalasr) + * feature #23805 [HttpKernel] Deprecated commands auto-registration (GuilhemN) + * feature #23816 [Debug] Detect internal and deprecated methods (GuilhemN) + * feature #23812 [FrameworkBundle] Allow micro kernel to subscribe events easily (ogizanagi) + * feature #22187 [DependencyInjection] Support local binding (GuilhemN) + * feature #23741 [DI] Generate one file per service factory (nicolas-grekas) + * feature #23807 [Debug] Trigger a deprecation when using an internal class/trait/interface (GuilhemN) + * feature #22587 [Workflow] Add transition completed event (izzyp) + * feature #23624 [FrameworkBundle] Commands as a service (ro0NL) + * feature #21111 [Validator] add groups support to the Valid constraint (xabbuh) + * feature #20361 [Config] Enable cannotBeEmpty along with requiresAtLeastOneElement (ro0NL) + * feature #23712 [DependencyInjection] Deprecate autowiring service auto-registration (GuilhemN) + * feature #23719 Autoconfigure instances of ArgumentValueResolverInterface (BPScott) + * feature #23706 [Webprofiler] Improve sql explain table display (mimol91) + * feature #23724 [Lock] Deprecate Filesystem/LockHandler (jderusse) + * feature #23593 [Workflow] Adding workflow name to the announce event (Nyholm) + * feature #20496 [Form] Allow pass filter callback to delete_empty option. (Koc) + * feature #23451 [Cache] Add (filesystem|phpfiles) cache (adapter|simple) prune method and prune command (robfrawley) + * feature #23519 [TwigBundle] Commands as a service (ro0NL) + * feature #23591 [VarDumper] Add time zone caster (maidmaid) + * feature #23510 [Console] Add a factory command loader for standalone application with lazy-loading needs (ogizanagi) + * feature #23357 [VarDumper] Add interval caster (maidmaid) + * feature #23550 [DebugBundle] Added min_depth to Configuration (james-johnston-thumbtack) + * feature #23570 [FrameworkBundle] Make RouterCacheWarmer implement ServiceSubscriberInterface (nicolas-grekas) + * feature #23437 [TwigBridge] deprecate TwigRenderer (Tobion) + * feature #23515 [VarDumper] Added setMinDepth to VarCloner (james-johnston-thumbtack) + * feature #23404 [Serializer] AbstractObjectNormalizer: Allow to disable type enforcement (ogizanagi) + * feature #21086 [MonologBridge] Add TokenProcessor (maidmaid) + * feature #22576 [Validator] Allow to use a property path to get value to compare in comparison constraints (ogizanagi) + * feature #22689 [DoctrineBridge] Add support for doctrin/dbal v2.6 types (jvasseur) + * feature #22734 [Console] Add support for command lazy-loading (chalasr) + * feature #19034 [Security] make it possible to configure a custom access decision manager service (xabbuh) + * feature #23037 [TwigBundle] Added a RuntimeExtensionInterface to take advantage of autoconfigure (lyrixx) + * feature #22176 [DI] Allow imports in string format for YAML (ro0NL) + * feature #23295 [Security] Lazy load user providers (chalasr) + * feature #23440 [Routing] Add matched and default parameters to redirect responses (artursvonda, Tobion) + * feature #22832 [Debug] Deprecate support for stacked errors (mbabker) + * feature #21469 [HttpFoundation] Find the original request protocol version (thewilkybarkid) + * feature #23431 [Validator] Add min/max amount of pixels to Image constraint (akeeman) + * feature #23223 Add support for microseconds in Stopwatch (javiereguiluz) + * feature #22341 [BrowserKit] Emulate back/forward browser navigation (e-moe) + * feature #22619 [FrameworkBundle][Translation] Move translation compiler pass (lepiaf) + * feature #22620 [FrameworkBundle][HttpKernel] Move httpkernel pass (lepiaf) + * feature #23337 [Component][Serializer][Normalizer] : Deal it with Has Method for the Normalizer/Denormalizer (jordscream) + * feature #22588 [VarDumper] Add filter in VarDumperTestTrait (maidmaid) + * feature #23288 [Yaml] deprecate the !str tag (xabbuh) + * feature #23039 [Validator] Support for parsing PHP constants in yaml loader (mimol91) + * feature #22431 [VarDumper] Add date caster (maidmaid) + * feature #23285 [Stopwatch] Add a reset method (jmgq) + * feature #23320 [WebServer] Allow * to bind all interfaces (as INADDR_ANY) (jpauli, fabpot) + * feature #23272 [FrameworkBundle] disable unusable fragment renderers (xabbuh) + * feature #23332 [Yaml] fix the displayed line number (fabpot, xabbuh) + * feature #23026 [SecurityBundle] Add user impersonation info and exit action to the profiler (yceruto) + * feature #22932 [HttpFoundation] Adds support for the immutable directive in the cache-control header (twoleds) + * feature #22554 [Profiler][Validator] Add a validator panel in profiler (ogizanagi) + * feature #22124 Shift responsibility for keeping Date header to ResponseHeaderBag (mpdude) + * feature #23122 Xml encoder optional type cast (ragboyjr) + * feature #23207 [FrameworkBundle] Allow .yaml file extension everywhere (ogizanagi) + * feature #23076 [Validator] Adds support to check specific DNS record type for URL (iisisrael) + * feature #22629 [Security] Trigger a deprecation when a voter is missing the VoterInterface (iltar) + * feature #22636 [Routing] Expose request in route conditions, if needed and possible (ro0NL) + * feature #22909 [Yaml] Deprecate using the non-specific tag (GuilhemN) + * feature #23042 Consistent error handling in remember me services (lstrojny) + * feature #22444 [Serializer] DateTimeNormalizer: allow to provide timezone (ogizanagi) + * feature #23143 [DI] Reference instead of inline for array-params (nicolas-grekas) + * feature #23154 [WebProfilerBundle] Sticky ajax window (ro0NL) + * feature #23161 [FrameworkBundle] Deprecate useless --no-prefix option (chalasr) + * feature #23105 [SecurityBundle][Profiler] Give info about called security listeners in profiler (chalasr) + * feature #23148 [FrameworkBundle] drop hard dependency on the Stopwatch component (xabbuh) + * feature #23131 [FrameworkBundle] Remove dependency on Doctrine cache (fabpot) + * feature #23114 [SecurityBundle] Lazy load security listeners (chalasr) + * feature #23111 [Process] Deprecate ProcessBuilder (nicolas-grekas) + * feature #22675 [FrameworkBundle] KernelTestCase: deprecate not using KERNEL_CLASS (ogizanagi) + * feature #22917 [VarDumper] Cycle prev/next searching in HTML dumps (ro0NL) + * feature #23044 Automatically enable the routing annotation loader (GuilhemN) + * feature #22696 [PropertyInfo] Made ReflectionExtractor's prefix lists instance variables (neemzy) + * feature #23035 Deprecate passing a concrete service in optional cache warmers (romainneutron) + * feature #23036 Implement ServiceSubscriberInterface in optional cache warmers (romainneutron) + * feature #23022 [Di] Remove closure-proxy arguments (nicolas-grekas) + * feature #22903 [DI] Deprecate XML services without ID (ro0NL) + * feature #22597 [Lock] Re-add the Lock component in 3.4 (jderusse) + * feature #22803 [DI] Deprecate Container::initialized() for privates (ro0NL) + * feature #22828 [Finder] Deprecate FilterIterator (ogizanagi) + * feature #22826 [Validator] improve strict option value deprecation (xabbuh) + diff --git a/UPGRADE-3.4.md b/UPGRADE-3.4.md index 5ef2448bf2..1b9a40c452 100644 --- a/UPGRADE-3.4.md +++ b/UPGRADE-3.4.md @@ -126,6 +126,8 @@ Form FrameworkBundle --------------- + * The `session.use_strict_mode` option has been deprecated and is enabled by default. + * The `cache:clear` command doesn't clear "app" PSR-6 cache pools anymore, but still clears "system" ones. Use the `cache:pool:clear` command to clear "app" pools instead. @@ -235,18 +237,13 @@ HttpFoundation * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler` class has been deprecated and will be removed in 4.0. Use the `\SessionHandler` class instead. - * The `Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy` class has been - deprecated and will be removed in 4.0. Use your `\SessionHandlerInterface` implementation directly. + * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler` class has been + deprecated and will be removed in 4.0. Implement `SessionUpdateTimestampHandlerInterface` or + extend `AbstractSessionHandler` instead. * The `Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy` class has been deprecated and will be removed in 4.0. Use your `\SessionHandlerInterface` implementation directly. - * The `Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy` class has been - deprecated and will be removed in 4.0. Use your `\SessionHandlerInterface` implementation directly. - - * `NativeSessionStorage::setSaveHandler()` now takes an instance of `\SessionHandlerInterface` as argument. - Not passing it is deprecated and will throw a `TypeError` in 4.0. - * Using `Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler` with the legacy mongo extension has been deprecated and will be removed in 4.0. Use it with the mongodb/mongodb package and ext-mongodb instead. diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index 7856656e0a..000fd2fc05 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -329,6 +329,8 @@ Form FrameworkBundle --------------- + * The `session.use_strict_mode` option has been removed and strict mode is always enabled. + * The `validator.mapping.cache.doctrine.apc` service has been removed. * The "framework.trusted_proxies" configuration option and the corresponding "kernel.trusted_proxies" parameter have been removed. Use the `Request::setTrustedProxies()` method in your front controller instead. @@ -542,12 +544,11 @@ HttpFoundation * The ability to check only for cacheable HTTP methods using `Request::isMethodSafe()` is not supported anymore, use `Request::isMethodCacheable()` instead. - * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler`, - `Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy`, - `Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy` and - `Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy` classes have been removed. + * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler` class has been + removed. Implement `SessionUpdateTimestampHandlerInterface` or extend `AbstractSessionHandler` instead. - * `NativeSessionStorage::setSaveHandler()` now requires an instance of `\SessionHandlerInterface` as argument. + * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler` and + `Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy` classes have been removed. * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler` does not work with the legacy mongo extension anymore. It requires mongodb/mongodb package and ext-mongodb. diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 48cc46fa8a..7160a372af 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -22,10 +22,12 @@ CHANGELOG * Removed the "framework.validation.cache" configuration option. Configure the "cache.validator" service under "framework.cache.pools" instead. * Removed `PhpStringTokenParser`, use `Symfony\Component\Translation\Extractor\PhpStringTokenParser` instead. * Removed `PhpExtractor`, use `Symfony\Component\Translation\Extractor\PhpExtractor` instead. + * Removed the `use_strict_mode` session option, it's is now enabled by default 3.4.0 ----- + * Session `use_strict_mode` is now enabled by default and the corresponding option has been deprecated * Made the `cache:clear` command to *not* clear "app" PSR-6 cache pools anymore, but to still clear "system" ones; use the `cache:pool:clear` command to clear "app" pools instead * Always register a minimalist logger that writes in `stderr` diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php new file mode 100644 index 0000000000..23d688495d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * A console command for autowiring information. + * + * @author Ryan Weaver + * + * @internal + */ +class DebugAutowiringCommand extends ContainerDebugCommand +{ + protected static $defaultName = 'debug:autowiring'; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDefinition(array( + new InputArgument('search', InputArgument::OPTIONAL, 'A search filter'), + )) + ->setDescription('Lists classes/interfaces you can use for autowiring') + ->setHelp(<<<'EOF' +The %command.name% command displays all classes and interfaces that +you can use as type-hints for autowiring: + + php %command.full_name% + +You can also pass a search term to filter the list: + + php %command.full_name% log + +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $errorIo = $io->getErrorStyle(); + + $builder = $this->getContainerBuilder(); + $serviceIds = $builder->getServiceIds(); + $serviceIds = array_filter($serviceIds, array($this, 'filterToServiceTypes')); + + if ($search = $input->getArgument('search')) { + $serviceIds = array_filter($serviceIds, function ($serviceId) use ($search) { + return false !== stripos($serviceId, $search); + }); + + if (empty($serviceIds)) { + $errorIo->error(sprintf('No autowirable classes or interfaces found matching "%s"', $search)); + + return 1; + } + } + + asort($serviceIds); + + $io->title('Autowirable Services'); + $io->text('The following classes & interfaces can be used as type-hints when autowiring:'); + if ($search) { + $io->text(sprintf('(only showing classes/interfaces matching %s)', $search)); + } + $io->newLine(); + $tableRows = array(); + foreach ($serviceIds as $serviceId) { + $tableRows[] = array(sprintf('%s', $serviceId)); + if ($builder->hasAlias($serviceId)) { + $tableRows[] = array(sprintf(' alias to %s', $builder->getAlias($serviceId))); + } + } + + $io->table(array(), $tableRows); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 36ce9b4086..563be13f49 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -413,11 +413,10 @@ class Configuration implements ConfigurationInterface ->scalarNode('gc_divisor')->end() ->scalarNode('gc_probability')->defaultValue(1)->end() ->scalarNode('gc_maxlifetime')->end() - ->booleanNode('use_strict_mode')->end() ->scalarNode('save_path')->defaultValue('%kernel.cache_dir%/sessions')->end() ->integerNode('metadata_update_threshold') ->defaultValue('0') - ->info('seconds to wait between 2 session metadata updates, it will also prevent the session handler to write if the session has not changed') + ->info('seconds to wait between 2 session metadata updates') ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 563aa1ab6d..57809f21b4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -728,7 +728,7 @@ class FrameworkExtension extends Extension // session storage $container->setAlias('session.storage', $config['storage_id'])->setPrivate(true); $options = array(); - foreach (array('name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor', 'use_strict_mode') as $key) { + foreach (array('name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor') as $key) { if (isset($config[$key])) { $options[$key] = $config[$key]; } @@ -742,14 +742,7 @@ class FrameworkExtension extends Extension $container->getDefinition('session.storage.native')->replaceArgument(1, null); $container->getDefinition('session.storage.php_bridge')->replaceArgument(0, null); } else { - $handlerId = $config['handler_id']; - - if ($config['metadata_update_threshold'] > 0) { - $container->getDefinition('session.handler.write_check')->addArgument(new Reference($handlerId)); - $handlerId = 'session.handler.write_check'; - } - - $container->setAlias('session.handler', $handlerId)->setPrivate(true); + $container->setAlias('session.handler', $config['handler_id'])->setPrivate(true); } $container->setParameter('session.save_path', $config['save_path']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml index b4d23a576e..9466d992c0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml @@ -111,5 +111,6 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml index d1977c5ebf..e926a106cb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml @@ -55,6 +55,10 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index 21c891881e..81af5c397c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml @@ -77,6 +77,7 @@ + %router.request_context.base_url% diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml index 335c14a8a6..f7903de647 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml @@ -48,12 +48,14 @@ - - %session.save_path% + + + + %session.save_path% + + - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php new file mode 100644 index 0000000000..0b7f9290a5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Tester\ApplicationTester; + +/** + * @group functional + */ +class DebugAutowiringCommandTest extends WebTestCase +{ + public function testBasicFunctionality() + { + static::bootKernel(array('test_case' => 'ContainerDebug', 'root_config' => 'config.yml')); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'debug:autowiring')); + + $this->assertContains('Symfony\Component\HttpKernel\HttpKernelInterface', $tester->getDisplay()); + $this->assertContains('alias to http_kernel', $tester->getDisplay()); + } + + public function testSearchArgument() + { + static::bootKernel(array('test_case' => 'ContainerDebug', 'root_config' => 'config.yml')); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'debug:autowiring', 'search' => 'kern')); + + $this->assertContains('Symfony\Component\HttpKernel\HttpKernelInterface', $tester->getDisplay()); + $this->assertNotContains('Symfony\Component\Routing\RouterInterface', $tester->getDisplay()); + } + + public function testSearchNoResults() + { + static::bootKernel(array('test_case' => 'ContainerDebug', 'root_config' => 'config.yml')); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'debug:autowiring', 'search' => 'foo_fake'), array('capture_stderr_separately' => true)); + + $this->assertContains('No autowirable classes or interfaces found matching "foo_fake"', $tester->getErrorOutput()); + $this->assertEquals(1, $tester->getStatusCode()); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 0225df0ee4..4ebbbb8542 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -52,6 +52,7 @@ + %security.authentication.trust_resolver.anonymous_class% diff --git a/src/Symfony/Component/Console/Logger/ConsoleLogger.php b/src/Symfony/Component/Console/Logger/ConsoleLogger.php index d7bcdfad00..ee2e185754 100644 --- a/src/Symfony/Component/Console/Logger/ConsoleLogger.php +++ b/src/Symfony/Component/Console/Logger/ConsoleLogger.php @@ -121,15 +121,23 @@ class ConsoleLogger extends AbstractLogger */ private function interpolate($message, array $context) { - // build a replacement array with braces around the context keys - $replace = array(); + if (false === strpos($message, '{')) { + return $message; + } + + $replacements = array(); foreach ($context as $key => $val) { - if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { - $replace[sprintf('{%s}', $key)] = $val; + if (null === $val || is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) { + $replacements["{{$key}}"] = $val; + } elseif ($val instanceof \DateTimeInterface) { + $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339); + } elseif (\is_object($val)) { + $replacements["{{$key}}"] = '[object '.\get_class($val).']'; + } else { + $replacements["{{$key}}"] = '['.\gettype($val).']'; } } - // interpolate replacement values into the message and return - return strtr($message, $replace); + return strtr($message, $replacements); } } diff --git a/src/Symfony/Component/Console/Tests/Logger/ConsoleLoggerTest.php b/src/Symfony/Component/Console/Tests/Logger/ConsoleLoggerTest.php index 342992982a..734a153e8a 100644 --- a/src/Symfony/Component/Console/Tests/Logger/ConsoleLoggerTest.php +++ b/src/Symfony/Component/Console/Tests/Logger/ConsoleLoggerTest.php @@ -166,9 +166,7 @@ class ConsoleLoggerTest extends TestCase } else { $dummy = $this->getMock('Symfony\Component\Console\Tests\Logger\DummyTest', array('__toString')); } - $dummy->expects($this->once()) - ->method('__toString') - ->will($this->returnValue('DUMMY')); + $dummy->method('__toString')->will($this->returnValue('DUMMY')); $this->getLogger()->warning($dummy); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php index 765b5752da..cbe9b30468 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php @@ -67,7 +67,6 @@ abstract class AbstractRecursivePass implements CompilerPassInterface $value->setArguments($this->processValue($value->getArguments())); $value->setProperties($this->processValue($value->getProperties())); $value->setMethodCalls($this->processValue($value->getMethodCalls())); - $value->setBindings($this->processValue($value->getBindings())); $changes = $value->getChanges(); if (isset($changes['factory'])) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php index dc20b39bc4..ac002d834d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\BoundArgument; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Compiler\CheckExceptionOnInvalidReferenceBehaviorPass; use Symfony\Component\DependencyInjection\Reference; @@ -88,6 +89,20 @@ class CheckExceptionOnInvalidReferenceBehaviorPassTest extends TestCase $this->process($container); } + public function testProcessDefinitionWithBindings() + { + $container = new ContainerBuilder(); + + $container + ->register('b') + ->setBindings(array(new BoundArgument(new Reference('a')))) + ; + + $this->process($container); + + $this->addToAssertionCount(1); + } + private function process(ContainerBuilder $container) { $pass = new CheckExceptionOnInvalidReferenceBehaviorPass(); diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 81e9bba4b9..5b90113d51 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -17,8 +17,7 @@ CHANGELOG * checking for cacheable HTTP methods using the `Request::isMethodSafe()` method (by not passing `false` as its argument) is not supported anymore and throws a `\BadMethodCallException` - * the `NativeSessionHandler` class has been removed - * the `AbstractProxy`, `NativeProxy` and `SessionHandlerProxy` classes have been removed + * the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes have been removed * setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()` is not supported anymore and throws a `\TypeError` @@ -26,8 +25,9 @@ CHANGELOG 3.4.0 ----- - * deprecated the `NativeSessionHandler` class, - * deprecated the `AbstractProxy`, `NativeProxy` and `SessionHandlerProxy` classes, + * implemented PHP 7.0's `SessionUpdateTimestampHandlerInterface` with a new + `AbstractSessionHandler` base class and a new `StrictSessionHandler` wrapper + * deprecated the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes * deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()` * deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead * deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/AbstractSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/AbstractSessionHandler.php new file mode 100644 index 0000000000..c20a23b20e --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/AbstractSessionHandler.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * This abstract session handler provides a generic implementation + * of the PHP 7.0 SessionUpdateTimestampHandlerInterface, + * enabling strict and lazy session handling. + * + * @author Nicolas Grekas + */ +abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + private $sessionName; + private $prefetchId; + private $prefetchData; + private $newSessionId; + private $igbinaryEmptyData; + + /** + * {@inheritdoc} + */ + public function open($savePath, $sessionName) + { + $this->sessionName = $sessionName; + + return true; + } + + /** + * @param string $sessionId + * + * @return string + */ + abstract protected function doRead($sessionId); + + /** + * @param string $sessionId + * @param string $data + * + * @return bool + */ + abstract protected function doWrite($sessionId, $data); + + /** + * @param string $sessionId + * + * @return bool + */ + abstract protected function doDestroy($sessionId); + + /** + * {@inheritdoc} + */ + public function validateId($sessionId) + { + $this->prefetchData = $this->read($sessionId); + $this->prefetchId = $sessionId; + + return '' !== $this->prefetchData; + } + + /** + * {@inheritdoc} + */ + public function read($sessionId) + { + if (null !== $this->prefetchId) { + $prefetchId = $this->prefetchId; + $prefetchData = $this->prefetchData; + $this->prefetchId = $this->prefetchData = null; + + if ($prefetchId === $sessionId || '' === $prefetchData) { + $this->newSessionId = '' === $prefetchData ? $sessionId : null; + + return $prefetchData; + } + } + + $data = $this->doRead($sessionId); + $this->newSessionId = '' === $data ? $sessionId : null; + if (\PHP_VERSION_ID < 70000) { + $this->prefetchData = $data; + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function write($sessionId, $data) + { + if (\PHP_VERSION_ID < 70000 && $this->prefetchData) { + $readData = $this->prefetchData; + $this->prefetchData = null; + + if ($readData === $data) { + return $this->updateTimestamp($sessionId, $data); + } + } + if (null === $this->igbinaryEmptyData) { + // see https://github.com/igbinary/igbinary/issues/146 + $this->igbinaryEmptyData = \function_exists('igbinary_serialize') ? igbinary_serialize(array()) : ''; + } + if ('' === $data || $this->igbinaryEmptyData === $data) { + return $this->destroy($sessionId); + } + $this->newSessionId = null; + + return $this->doWrite($sessionId, $data); + } + + /** + * {@inheritdoc} + */ + public function destroy($sessionId) + { + if (\PHP_VERSION_ID < 70000) { + $this->prefetchData = null; + } + if (!headers_sent() && ini_get('session.use_cookies')) { + if (!$this->sessionName) { + throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', get_class($this))); + } + $sessionCookie = sprintf(' %s=', urlencode($this->sessionName)); + $sessionCookieWithId = sprintf('%s%s;', $sessionCookie, urlencode($sessionId)); + $sessionCookieFound = false; + $otherCookies = array(); + foreach (headers_list() as $h) { + if (0 !== stripos($h, 'Set-Cookie:')) { + continue; + } + if (11 === strpos($h, $sessionCookie, 11)) { + $sessionCookieFound = true; + + if (11 !== strpos($h, $sessionCookieWithId, 11)) { + $otherCookies[] = $h; + } + } else { + $otherCookies[] = $h; + } + } + if ($sessionCookieFound) { + header_remove('Set-Cookie'); + foreach ($otherCookies as $h) { + header('Set-Cookie:'.$h, false); + } + } else { + setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure'), ini_get('session.cookie_httponly')); + } + } + + return $this->newSessionId === $sessionId || $this->doDestroy($sessionId); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php index 3bbde5420d..a31642cc83 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php @@ -19,7 +19,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; * * @author Drak */ -class MemcachedSessionHandler implements \SessionHandlerInterface +class MemcachedSessionHandler extends AbstractSessionHandler { /** * @var \Memcached Memcached driver @@ -39,7 +39,7 @@ class MemcachedSessionHandler implements \SessionHandlerInterface /** * List of available options: * * prefix: The prefix to use for the memcached keys in order to avoid collision - * * expiretime: The time to live in seconds + * * expiretime: The time to live in seconds. * * @param \Memcached $memcached A \Memcached instance * @param array $options An associative array of Memcached options @@ -60,14 +60,6 @@ class MemcachedSessionHandler implements \SessionHandlerInterface $this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s'; } - /** - * {@inheritdoc} - */ - public function open($savePath, $sessionName) - { - return true; - } - /** * {@inheritdoc} */ @@ -79,7 +71,7 @@ class MemcachedSessionHandler implements \SessionHandlerInterface /** * {@inheritdoc} */ - public function read($sessionId) + protected function doRead($sessionId) { return $this->memcached->get($this->prefix.$sessionId) ?: ''; } @@ -87,7 +79,15 @@ class MemcachedSessionHandler implements \SessionHandlerInterface /** * {@inheritdoc} */ - public function write($sessionId, $data) + public function updateTimestamp($sessionId, $data) + { + return $this->memcached->touch($this->prefix.$sessionId, time() + $this->ttl); + } + + /** + * {@inheritdoc} + */ + protected function doWrite($sessionId, $data) { return $this->memcached->set($this->prefix.$sessionId, $data, time() + $this->ttl); } @@ -95,7 +95,7 @@ class MemcachedSessionHandler implements \SessionHandlerInterface /** * {@inheritdoc} */ - public function destroy($sessionId) + protected function doDestroy($sessionId) { $result = $this->memcached->delete($this->prefix.$sessionId); diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php index e1bb64661b..05198de2c7 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php @@ -19,7 +19,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; * @see https://packagist.org/packages/mongodb/mongodb * @see http://php.net/manual/en/set.mongodb.php */ -class MongoDbSessionHandler implements \SessionHandlerInterface +class MongoDbSessionHandler extends AbstractSessionHandler { /** * @var \MongoDB\Client @@ -43,7 +43,7 @@ class MongoDbSessionHandler implements \SessionHandlerInterface * * id_field: The field name for storing the session id [default: _id] * * data_field: The field name for storing the session data [default: data] * * time_field: The field name for storing the timestamp [default: time] - * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at] + * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. * * It is strongly recommended to put an index on the `expiry_field` for * garbage-collection. Alternatively it's possible to automatically expire @@ -83,14 +83,6 @@ class MongoDbSessionHandler implements \SessionHandlerInterface ), $options); } - /** - * {@inheritdoc} - */ - public function open($savePath, $sessionName) - { - return true; - } - /** * {@inheritdoc} */ @@ -102,7 +94,7 @@ class MongoDbSessionHandler implements \SessionHandlerInterface /** * {@inheritdoc} */ - public function destroy($sessionId) + protected function doDestroy($sessionId) { $this->getCollection()->deleteOne(array( $this->options['id_field'] => $sessionId, @@ -126,7 +118,7 @@ class MongoDbSessionHandler implements \SessionHandlerInterface /** * {@inheritdoc} */ - public function write($sessionId, $data) + protected function doWrite($sessionId, $data) { $expiry = new \MongoDB\BSON\UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000); @@ -148,7 +140,34 @@ class MongoDbSessionHandler implements \SessionHandlerInterface /** * {@inheritdoc} */ - public function read($sessionId) + public function updateTimestamp($sessionId, $data) + { + $expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime')); + + if ($this->mongo instanceof \MongoDB\Client) { + $methodName = 'updateOne'; + $options = array(); + } else { + $methodName = 'update'; + $options = array('multiple' => false); + } + + $this->getCollection()->$methodName( + array($this->options['id_field'] => $sessionId), + array('$set' => array( + $this->options['time_field'] => $this->createDateTime(), + $this->options['expiry_field'] => $expiry, + )), + $options + ); + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doRead($sessionId) { $dbData = $this->getCollection()->findOne(array( $this->options['id_field'] => $sessionId, diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/NullSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/NullSessionHandler.php index 981d96d93a..8d193155b0 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/NullSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/NullSessionHandler.php @@ -16,16 +16,8 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; * * @author Drak */ -class NullSessionHandler implements \SessionHandlerInterface +class NullSessionHandler extends AbstractSessionHandler { - /** - * {@inheritdoc} - */ - public function open($savePath, $sessionName) - { - return true; - } - /** * {@inheritdoc} */ @@ -37,15 +29,7 @@ class NullSessionHandler implements \SessionHandlerInterface /** * {@inheritdoc} */ - public function read($sessionId) - { - return ''; - } - - /** - * {@inheritdoc} - */ - public function write($sessionId, $data) + public function validateId($sessionId) { return true; } @@ -53,7 +37,31 @@ class NullSessionHandler implements \SessionHandlerInterface /** * {@inheritdoc} */ - public function destroy($sessionId) + protected function doRead($sessionId) + { + return ''; + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + return true; + } + + /** + * {@inheritdoc} + */ + protected function doWrite($sessionId, $data) + { + return true; + } + + /** + * {@inheritdoc} + */ + protected function doDestroy($sessionId) { return true; } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index 5cdac63939..19bf6e9bca 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -38,7 +38,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; * @author Michael Williams * @author Tobias Schultze */ -class PdoSessionHandler implements \SessionHandlerInterface +class PdoSessionHandler extends AbstractSessionHandler { /** * No locking is done. This means sessions are prone to loss of data due to @@ -260,11 +260,13 @@ class PdoSessionHandler implements \SessionHandlerInterface */ public function open($savePath, $sessionName) { + $this->sessionExpired = false; + if (null === $this->pdo) { $this->connect($this->dsn ?: $savePath); } - return true; + return parent::open($savePath, $sessionName); } /** @@ -273,7 +275,7 @@ class PdoSessionHandler implements \SessionHandlerInterface public function read($sessionId) { try { - return $this->doRead($sessionId); + return parent::read($sessionId); } catch (\PDOException $e) { $this->rollback(); @@ -296,7 +298,7 @@ class PdoSessionHandler implements \SessionHandlerInterface /** * {@inheritdoc} */ - public function destroy($sessionId) + protected function doDestroy($sessionId) { // delete the record associated with this id $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; @@ -317,7 +319,7 @@ class PdoSessionHandler implements \SessionHandlerInterface /** * {@inheritdoc} */ - public function write($sessionId, $data) + protected function doWrite($sessionId, $data) { $maxlifetime = (int) ini_get('session.gc_maxlifetime'); @@ -372,6 +374,30 @@ class PdoSessionHandler implements \SessionHandlerInterface return true; } + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + $maxlifetime = (int) ini_get('session.gc_maxlifetime'); + + try { + $updateStmt = $this->pdo->prepare( + "UPDATE $this->table SET $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id" + ); + $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); + $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $updateStmt->execute(); + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + + return true; + } + /** * {@inheritdoc} */ @@ -491,10 +517,8 @@ class PdoSessionHandler implements \SessionHandlerInterface * * @return string The session data */ - private function doRead($sessionId) + protected function doRead($sessionId) { - $this->sessionExpired = false; - if (self::LOCK_ADVISORY === $this->lockMode) { $this->unlockStatements[] = $this->doAdvisoryLock($sessionId); } @@ -517,7 +541,9 @@ class PdoSessionHandler implements \SessionHandlerInterface return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; } - if (self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { + if (!ini_get('session.use_strict_mode') && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { + // In strict mode, session fixation is not possible: new sessions always start with a unique + // random id, so that concurrency is not possible and this code path can be skipped. // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block // until other connections to the session are committed. try { diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php new file mode 100644 index 0000000000..1bad0641e8 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`. + * + * @author Nicolas Grekas + */ +class StrictSessionHandler extends AbstractSessionHandler +{ + private $handler; + + public function __construct(\SessionHandlerInterface $handler) + { + if ($handler instanceof \SessionUpdateTimestampHandlerInterface) { + throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_class($handler), self::class)); + } + + $this->handler = $handler; + } + + /** + * {@inheritdoc} + */ + public function open($savePath, $sessionName) + { + parent::open($savePath, $sessionName); + + return $this->handler->open($savePath, $sessionName); + } + + /** + * {@inheritdoc} + */ + protected function doRead($sessionId) + { + return $this->handler->read($sessionId); + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + return $this->write($sessionId, $data); + } + + /** + * {@inheritdoc} + */ + protected function doWrite($sessionId, $data) + { + return $this->handler->write($sessionId, $data); + } + + /** + * {@inheritdoc} + */ + protected function doDestroy($sessionId) + { + return $this->handler->destroy($sessionId); + } + + /** + * {@inheritdoc} + */ + public function close() + { + return $this->handler->close(); + } + + /** + * {@inheritdoc} + */ + public function gc($maxlifetime) + { + return $this->handler->gc($maxlifetime); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/WriteCheckSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/WriteCheckSessionHandler.php deleted file mode 100644 index d49c36cae5..0000000000 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/WriteCheckSessionHandler.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; - -/** - * Wraps another SessionHandlerInterface to only write the session when it has been modified. - * - * @author Adrien Brault - */ -class WriteCheckSessionHandler implements \SessionHandlerInterface -{ - /** - * @var \SessionHandlerInterface - */ - private $wrappedSessionHandler; - - /** - * @var array sessionId => session - */ - private $readSessions; - - public function __construct(\SessionHandlerInterface $wrappedSessionHandler) - { - $this->wrappedSessionHandler = $wrappedSessionHandler; - } - - /** - * {@inheritdoc} - */ - public function close() - { - return $this->wrappedSessionHandler->close(); - } - - /** - * {@inheritdoc} - */ - public function destroy($sessionId) - { - return $this->wrappedSessionHandler->destroy($sessionId); - } - - /** - * {@inheritdoc} - */ - public function gc($maxlifetime) - { - return $this->wrappedSessionHandler->gc($maxlifetime); - } - - /** - * {@inheritdoc} - */ - public function open($savePath, $sessionName) - { - return $this->wrappedSessionHandler->open($savePath, $sessionName); - } - - /** - * {@inheritdoc} - */ - public function read($sessionId) - { - $session = $this->wrappedSessionHandler->read($sessionId); - - $this->readSessions[$sessionId] = $session; - - return $session; - } - - /** - * {@inheritdoc} - */ - public function write($sessionId, $data) - { - if (isset($this->readSessions[$sessionId]) && $data === $this->readSessions[$sessionId]) { - return true; - } - - return $this->wrappedSessionHandler->write($sessionId, $data); - } -} diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php b/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php index 07f556ca60..338a4f53f1 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php @@ -12,6 +12,9 @@ namespace Symfony\Component\HttpFoundation\Session\Storage; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; /** * This provides a base class for session attribute storage. @@ -23,7 +26,7 @@ class NativeSessionStorage implements SessionStorageInterface /** * @var SessionBagInterface[] */ - protected $bags; + protected $bags = array(); /** * @var bool @@ -36,7 +39,7 @@ class NativeSessionStorage implements SessionStorageInterface protected $closed = false; /** - * @var \SessionHandlerInterface + * @var AbstractProxy|\SessionHandlerInterface */ protected $saveHandler; @@ -85,13 +88,18 @@ class NativeSessionStorage implements SessionStorageInterface * sid_bits_per_character, "5" * trans_sid_hosts, $_SERVER['HTTP_HOST'] * trans_sid_tags, "a=href,area=href,frame=src,form=" + * + * @param array $options Session configuration options + * @param \SessionHandlerInterface|null $handler + * @param MetadataBag $metaBag MetadataBag */ - public function __construct(array $options = array(), \SessionHandlerInterface $handler = null, MetadataBag $metaBag = null) + public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null) { $options += array( - // disable by default because it's managed by HeaderBag (if used) - 'cache_limiter' => '', + 'cache_limiter' => 'private_no_expire', 'use_cookies' => 1, + 'lazy_write' => 1, + 'use_strict_mode' => 1, ); session_register_shutdown(); @@ -104,7 +112,7 @@ class NativeSessionStorage implements SessionStorageInterface /** * Gets the save handler instance. * - * @return \SessionHandlerInterface + * @return AbstractProxy|\SessionHandlerInterface */ public function getSaveHandler() { @@ -143,21 +151,15 @@ class NativeSessionStorage implements SessionStorageInterface */ public function getId() { - return session_id(); + return $this->saveHandler->getId(); } /** * {@inheritdoc} - * - * @throws \LogicException When the session is active */ public function setId($id) { - if (\PHP_SESSION_ACTIVE === session_status()) { - throw new \LogicException('Cannot change the ID of an active session'); - } - - session_id($id); + $this->saveHandler->setId($id); } /** @@ -165,21 +167,15 @@ class NativeSessionStorage implements SessionStorageInterface */ public function getName() { - return session_name(); + return $this->saveHandler->getName(); } /** * {@inheritdoc} - * - * @throws \LogicException When the session is active */ public function setName($name) { - if (\PHP_SESSION_ACTIVE === session_status()) { - throw new \LogicException('Cannot change the name of an active session'); - } - - session_name($name); + $this->saveHandler->setName($name); } /** @@ -218,20 +214,38 @@ class NativeSessionStorage implements SessionStorageInterface */ public function save() { + $session = $_SESSION; + + foreach ($this->bags as $bag) { + if (empty($_SESSION[$key = $bag->getStorageKey()])) { + unset($_SESSION[$key]); + } + } + if (array($key = $this->metadataBag->getStorageKey()) === array_keys($_SESSION)) { + unset($_SESSION[$key]); + } + // Register custom error handler to catch a possible failure warning during session write - set_error_handler(function ($errno, $errstr, $errfile, $errline, $errcontext) { - throw new \ErrorException($errstr, $errno, E_WARNING, $errfile, $errline, $errcontext); + set_error_handler(function ($errno, $errstr, $errfile, $errline) { + throw new \ErrorException($errstr, $errno, E_WARNING, $errfile, $errline); }, E_WARNING); try { + $e = null; session_write_close(); - restore_error_handler(); } catch (\ErrorException $e) { + } finally { + restore_error_handler(); + $_SESSION = $session; + } + if (null !== $e) { // The default PHP error message is not very helpful, as it does not give any information on the current save handler. // Therefore, we catch this error and trigger a warning with a better error message $handler = $this->getSaveHandler(); + if ($handler instanceof SessionHandlerProxy) { + $handler = $handler->getHandler(); + } - restore_error_handler(); trigger_error(sprintf('session_write_close(): Failed to write session data with %s handler', get_class($handler)), E_USER_WARNING); } @@ -258,8 +272,6 @@ class NativeSessionStorage implements SessionStorageInterface /** * {@inheritdoc} - * - * @throws \LogicException When the session is already started */ public function registerBag(SessionBagInterface $bag) { @@ -279,7 +291,7 @@ class NativeSessionStorage implements SessionStorageInterface throw new \InvalidArgumentException(sprintf('The SessionBagInterface %s is not registered.', $name)); } - if (!$this->started && \PHP_SESSION_ACTIVE === session_status()) { + if (!$this->started && $this->saveHandler->isActive()) { $this->loadSession(); } elseif (!$this->started) { $this->start(); @@ -372,16 +384,36 @@ class NativeSessionStorage implements SessionStorageInterface * @see http://php.net/sessionhandlerinterface * @see http://php.net/sessionhandler * @see http://github.com/drak/NativeSession + * + * @param \SessionHandlerInterface|null $saveHandler + * + * @throws \InvalidArgumentException */ - public function setSaveHandler(\SessionHandlerInterface $saveHandler = null) + public function setSaveHandler($saveHandler = null) { - $this->saveHandler = $saveHandler ?: new \SessionHandler(); + if (!$saveHandler instanceof AbstractProxy && + !$saveHandler instanceof \SessionHandlerInterface && + null !== $saveHandler) { + throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.'); + } if (headers_sent($file, $line)) { throw new \RuntimeException(sprintf('Failed to set the session handler because headers have already been sent by "%s" at line %d.', $file, $line)); } - session_set_save_handler($this->saveHandler, false); + // Wrap $saveHandler in proxy and prevent double wrapping of proxy + if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) { + $saveHandler = new SessionHandlerProxy($saveHandler); + } elseif (!$saveHandler instanceof AbstractProxy) { + $saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler())); + } + $this->saveHandler = $saveHandler; + + if ($this->saveHandler instanceof SessionHandlerProxy) { + session_set_save_handler($this->saveHandler->getHandler(), false); + } elseif ($this->saveHandler instanceof \SessionHandlerInterface) { + session_set_save_handler($this->saveHandler, false); + } } /** @@ -400,12 +432,11 @@ class NativeSessionStorage implements SessionStorageInterface $session = &$_SESSION; } - $bags = $this->bags; - $bags[] = $this->metadataBag; + $bags = array_merge($this->bags, array($this->metadataBag)); foreach ($bags as $bag) { $key = $bag->getStorageKey(); - $session[$key] = $session[$key] ?? array(); + $session[$key] = isset($session[$key]) ? $session[$key] : array(); $bag->initialize($session[$key]); } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/AbstractProxy.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/AbstractProxy.php new file mode 100644 index 0000000000..09c92483c7 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/AbstractProxy.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; + +/** + * @author Drak + */ +abstract class AbstractProxy +{ + /** + * Flag if handler wraps an internal PHP session handler (using \SessionHandler). + * + * @var bool + */ + protected $wrapper = false; + + /** + * @var string + */ + protected $saveHandlerName; + + /** + * Gets the session.save_handler name. + * + * @return string + */ + public function getSaveHandlerName() + { + return $this->saveHandlerName; + } + + /** + * Is this proxy handler and instance of \SessionHandlerInterface. + * + * @return bool + */ + public function isSessionHandlerInterface() + { + return $this instanceof \SessionHandlerInterface; + } + + /** + * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. + * + * @return bool + */ + public function isWrapper() + { + return $this->wrapper; + } + + /** + * Has a session started? + * + * @return bool + */ + public function isActive() + { + return \PHP_SESSION_ACTIVE === session_status(); + } + + /** + * Gets the session ID. + * + * @return string + */ + public function getId() + { + return session_id(); + } + + /** + * Sets the session ID. + * + * @param string $id + * + * @throws \LogicException + */ + public function setId($id) + { + if ($this->isActive()) { + throw new \LogicException('Cannot change the ID of an active session'); + } + + session_id($id); + } + + /** + * Gets the session name. + * + * @return string + */ + public function getName() + { + return session_name(); + } + + /** + * Sets the session name. + * + * @param string $name + * + * @throws \LogicException + */ + public function setName($name) + { + if ($this->isActive()) { + throw new \LogicException('Cannot change the name of an active session'); + } + + session_name($name); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php new file mode 100644 index 0000000000..359bb877b5 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; + +/** + * @author Drak + */ +class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface +{ + /** + * @var \SessionHandlerInterface + */ + protected $handler; + + public function __construct(\SessionHandlerInterface $handler) + { + $this->handler = $handler; + $this->wrapper = ($handler instanceof \SessionHandler); + $this->saveHandlerName = $this->wrapper ? ini_get('session.save_handler') : 'user'; + } + + /** + * @return \SessionHandlerInterface + */ + public function getHandler() + { + return $this->handler; + } + + // \SessionHandlerInterface + + /** + * {@inheritdoc} + */ + public function open($savePath, $sessionName) + { + return (bool) $this->handler->open($savePath, $sessionName); + } + + /** + * {@inheritdoc} + */ + public function close() + { + return (bool) $this->handler->close(); + } + + /** + * {@inheritdoc} + */ + public function read($sessionId) + { + return (string) $this->handler->read($sessionId); + } + + /** + * {@inheritdoc} + */ + public function write($sessionId, $data) + { + return (bool) $this->handler->write($sessionId, $data); + } + + /** + * {@inheritdoc} + */ + public function destroy($sessionId) + { + return (bool) $this->handler->destroy($sessionId); + } + + /** + * {@inheritdoc} + */ + public function gc($maxlifetime) + { + return (bool) $this->handler->gc($maxlifetime); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php new file mode 100644 index 0000000000..3ac081e388 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; + +use PHPUnit\Framework\TestCase; + +/** + * @requires PHP 7.0 + */ +class AbstractSessionHandlerTest extends TestCase +{ + private static $server; + + public static function setUpBeforeClass() + { + $spec = array( + 1 => array('file', '/dev/null', 'w'), + 2 => array('file', '/dev/null', 'w'), + ); + if (!self::$server = @proc_open('exec php -S localhost:8053', $spec, $pipes, __DIR__.'/Fixtures')) { + self::markTestSkipped('PHP server unable to start.'); + } + sleep(1); + } + + public static function tearDownAfterClass() + { + if (self::$server) { + proc_terminate(self::$server); + proc_close(self::$server); + } + } + + /** + * @dataProvider provideSession + */ + public function testSession($fixture) + { + $context = array('http' => array('header' => "Cookie: sid=123abc\r\n")); + $context = stream_context_create($context); + $result = file_get_contents(sprintf('http://localhost:8053/%s.php', $fixture), false, $context); + + $this->assertStringEqualsFile(__DIR__.sprintf('/Fixtures/%s.expected', $fixture), $result); + } + + public function provideSession() + { + foreach (glob(__DIR__.'/Fixtures/*.php') as $file) { + yield array(pathinfo($file, PATHINFO_FILENAME)); + } + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/common.inc b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/common.inc new file mode 100644 index 0000000000..5c183acfff --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/common.inc @@ -0,0 +1,152 @@ +data = $data; + } + + public function open($path, $name) + { + echo __FUNCTION__, "\n"; + + return parent::open($path, $name); + } + + public function validateId($sessionId) + { + echo __FUNCTION__, "\n"; + + return parent::validateId($sessionId); + } + + /** + * {@inheritdoc} + */ + public function read($sessionId) + { + echo __FUNCTION__, "\n"; + + return parent::read($sessionId); + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + echo __FUNCTION__, "\n"; + + return true; + } + + /** + * {@inheritdoc} + */ + public function write($sessionId, $data) + { + echo __FUNCTION__, "\n"; + + return parent::write($sessionId, $data); + } + + /** + * {@inheritdoc} + */ + public function destroy($sessionId) + { + echo __FUNCTION__, "\n"; + + return parent::destroy($sessionId); + } + + public function close() + { + echo __FUNCTION__, "\n"; + + return true; + } + + public function gc($maxLifetime) + { + echo __FUNCTION__, "\n"; + + return true; + } + + protected function doRead($sessionId) + { + echo __FUNCTION__.': ', $this->data, "\n"; + + return $this->data; + } + + protected function doWrite($sessionId, $data) + { + echo __FUNCTION__.': ', $data, "\n"; + + return true; + } + + protected function doDestroy($sessionId) + { + echo __FUNCTION__, "\n"; + + return true; + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected new file mode 100644 index 0000000000..1720bf0558 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected @@ -0,0 +1,17 @@ +open +validateId +read +doRead: abc|i:123; +read + +write +destroy +doDestroy +close +Array +( + [0] => Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: private, max-age=10800 + [2] => Set-Cookie: sid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly +) +shutdown diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.php new file mode 100644 index 0000000000..3cfc1250ad --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.php @@ -0,0 +1,8 @@ + Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: private, max-age=10800 +) +shutdown diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/read_only.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/read_only.php new file mode 100644 index 0000000000..3e62fb9ecb --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/read_only.php @@ -0,0 +1,8 @@ + Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: private, max-age=10800 + [2] => Set-Cookie: sid=random_session_id; path=/; secure; HttpOnly +) +shutdown diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/regenerate.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/regenerate.php new file mode 100644 index 0000000000..a0f635c871 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/regenerate.php @@ -0,0 +1,10 @@ + bar +) +$_SESSION is not empty +write +destroy +close +$_SESSION is not empty +Array +( + [0] => Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: private, max-age=10800 +) +shutdown diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.php new file mode 100644 index 0000000000..96dca3c2c0 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.php @@ -0,0 +1,24 @@ +setSaveHandler(new TestSessionHandler()); +$flash = new FlashBag(); +$storage->registerBag($flash); +$storage->start(); + +$flash->add('foo', 'bar'); + +print_r($flash->get('foo')); +echo empty($_SESSION) ? '$_SESSION is empty' : '$_SESSION is not empty'; +echo "\n"; + +$storage->save(); + +echo empty($_SESSION) ? '$_SESSION is empty' : '$_SESSION is not empty'; + +ob_start(function ($buffer) { return str_replace(session_id(), 'random_session_id', $buffer); }); diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie.expected b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie.expected new file mode 100644 index 0000000000..47ae4da824 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie.expected @@ -0,0 +1,15 @@ +open +validateId +read +doRead: abc|i:123; +read + +updateTimestamp +close +Array +( + [0] => Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: private, max-age=10800 + [2] => Set-Cookie: abc=def +) +shutdown diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie.php new file mode 100644 index 0000000000..ffb5b20a37 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie.php @@ -0,0 +1,8 @@ +markTestSkipped('Strict mode needs no locking for new sessions.'); + } + $pdo = new MockPdo('pgsql'); $selectStmt = $this->getMockBuilder('PDOStatement')->getMock(); $insertStmt = $this->getMockBuilder('PDOStatement')->getMock(); diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/StrictSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/StrictSessionHandlerTest.php new file mode 100644 index 0000000000..9d2c1949f3 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/StrictSessionHandlerTest.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; + +class StrictSessionHandlerTest extends TestCase +{ + public function testOpen() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('open') + ->with('path', 'name')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertInstanceof('SessionUpdateTimestampHandlerInterface', $proxy); + $this->assertInstanceof(AbstractSessionHandler::class, $proxy); + $this->assertTrue($proxy->open('path', 'name')); + } + + public function testCloseSession() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('close') + ->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->close()); + } + + public function testValidateIdOK() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn('data'); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->validateId('id')); + } + + public function testValidateIdKO() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn(''); + $proxy = new StrictSessionHandler($handler); + + $this->assertFalse($proxy->validateId('id')); + } + + public function testRead() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn('data'); + $proxy = new StrictSessionHandler($handler); + + $this->assertSame('data', $proxy->read('id')); + } + + public function testReadWithValidateIdOK() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn('data'); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->validateId('id')); + $this->assertSame('data', $proxy->read('id')); + } + + public function testReadWithValidateIdMismatch() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->exactly(2))->method('read') + ->withConsecutive(array('id1'), array('id2')) + ->will($this->onConsecutiveCalls('data1', 'data2')); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->validateId('id1')); + $this->assertSame('data2', $proxy->read('id2')); + } + + public function testUpdateTimestamp() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('write') + ->with('id', 'data')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->updateTimestamp('id', 'data')); + } + + public function testWrite() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('write') + ->with('id', 'data')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->write('id', 'data')); + } + + public function testWriteEmptyNewSession() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn(''); + $handler->expects($this->never())->method('write'); + $handler->expects($this->never())->method('destroy'); + $proxy = new StrictSessionHandler($handler); + + $this->assertFalse($proxy->validateId('id')); + $this->assertSame('', $proxy->read('id')); + $this->assertTrue($proxy->write('id', '')); + } + + public function testWriteEmptyExistingSession() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn('data'); + $handler->expects($this->never())->method('write'); + $handler->expects($this->once())->method('destroy')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertSame('data', $proxy->read('id')); + $this->assertTrue($proxy->write('id', '')); + } + + public function testDestroy() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('destroy') + ->with('id')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->destroy('id')); + } + + public function testDestroyNewSession() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn(''); + $handler->expects($this->never())->method('destroy'); + $proxy = new StrictSessionHandler($handler); + + $this->assertSame('', $proxy->read('id')); + $this->assertTrue($proxy->destroy('id')); + } + + public function testDestroyNonEmptyNewSession() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('read') + ->with('id')->willReturn(''); + $handler->expects($this->once())->method('write') + ->with('id', 'data')->willReturn(true); + $handler->expects($this->once())->method('destroy') + ->with('id')->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertSame('', $proxy->read('id')); + $this->assertTrue($proxy->write('id', 'data')); + $this->assertTrue($proxy->destroy('id')); + } + + public function testGc() + { + $handler = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $handler->expects($this->once())->method('gc') + ->with(123)->willReturn(true); + $proxy = new StrictSessionHandler($handler); + + $this->assertTrue($proxy->gc(123)); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php deleted file mode 100644 index 5e41a4743e..0000000000 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php +++ /dev/null @@ -1,95 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler; - -/** - * @author Adrien Brault - */ -class WriteCheckSessionHandlerTest extends TestCase -{ - public function test() - { - $wrappedSessionHandlerMock = $this->getMockBuilder('SessionHandlerInterface')->getMock(); - $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); - - $wrappedSessionHandlerMock - ->expects($this->once()) - ->method('close') - ->with() - ->will($this->returnValue(true)) - ; - - $this->assertTrue($writeCheckSessionHandler->close()); - } - - public function testWrite() - { - $wrappedSessionHandlerMock = $this->getMockBuilder('SessionHandlerInterface')->getMock(); - $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); - - $wrappedSessionHandlerMock - ->expects($this->once()) - ->method('write') - ->with('foo', 'bar') - ->will($this->returnValue(true)) - ; - - $this->assertTrue($writeCheckSessionHandler->write('foo', 'bar')); - } - - public function testSkippedWrite() - { - $wrappedSessionHandlerMock = $this->getMockBuilder('SessionHandlerInterface')->getMock(); - $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); - - $wrappedSessionHandlerMock - ->expects($this->once()) - ->method('read') - ->with('foo') - ->will($this->returnValue('bar')) - ; - - $wrappedSessionHandlerMock - ->expects($this->never()) - ->method('write') - ; - - $this->assertEquals('bar', $writeCheckSessionHandler->read('foo')); - $this->assertTrue($writeCheckSessionHandler->write('foo', 'bar')); - } - - public function testNonSkippedWrite() - { - $wrappedSessionHandlerMock = $this->getMockBuilder('SessionHandlerInterface')->getMock(); - $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); - - $wrappedSessionHandlerMock - ->expects($this->once()) - ->method('read') - ->with('foo') - ->will($this->returnValue('bar')) - ; - - $wrappedSessionHandlerMock - ->expects($this->once()) - ->method('write') - ->with('foo', 'baZZZ') - ->will($this->returnValue(true)) - ; - - $this->assertEquals('bar', $writeCheckSessionHandler->read('foo')); - $this->assertTrue($writeCheckSessionHandler->write('foo', 'baZZZ')); - } -} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php index 067fc392e8..93de175fd3 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php @@ -14,8 +14,10 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; /** * Test class for NativeSessionStorage. @@ -98,7 +100,6 @@ class NativeSessionStorageTest extends TestCase $storage->start(); $id = $storage->getId(); - $this->assertEquals(session_id(), $id); $this->assertInternalType('string', $id); $this->assertNotSame('', $id); @@ -106,44 +107,6 @@ class NativeSessionStorageTest extends TestCase $this->assertSame($id, $storage->getId(), 'ID stays after saving session'); } - public function testId() - { - $storage = $this->getStorage(); - - $this->assertEquals(session_id(), $storage->getId()); - $storage->setId('foo'); - $this->assertEquals('foo', $storage->getId()); - $this->assertEquals(session_id(), $storage->getId()); - } - - /** - * @expectedException \LogicException - */ - public function testIdException() - { - session_start(); - $this->getStorage()->setId('foo'); - } - - public function testName() - { - $storage = $this->getStorage(); - - $this->assertEquals(session_name(), $storage->getName()); - $storage->setName('foo'); - $this->assertEquals('foo', $storage->getName()); - $this->assertEquals(session_name(), $storage->getName()); - } - - /** - * @expectedException \LogicException - */ - public function testNameException() - { - session_start(); - $this->getStorage()->setName('foo'); - } - public function testRegenerate() { $storage = $this->getStorage(); @@ -189,7 +152,7 @@ class NativeSessionStorageTest extends TestCase $this->iniSet('session.cache_limiter', 'nocache'); $storage = new NativeSessionStorage(); - $this->assertEquals('', ini_get('session.cache_limiter')); + $this->assertEquals('private_no_expire', ini_get('session.cache_limiter')); } public function testExplicitSessionCacheLimiter() @@ -222,9 +185,9 @@ class NativeSessionStorageTest extends TestCase } /** - * @expectedException \TypeError + * @expectedException \InvalidArgumentException */ - public function testSetSaveHandlerError() + public function testSetSaveHandlerException() { $storage = $this->getStorage(); $storage->setSaveHandler(new \stdClass()); @@ -235,13 +198,17 @@ class NativeSessionStorageTest extends TestCase $this->iniSet('session.save_handler', 'files'); $storage = $this->getStorage(); $storage->setSaveHandler(); - $this->assertInstanceOf(\SessionHandler::class, $storage->getSaveHandler()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); $storage->setSaveHandler(null); - $this->assertInstanceOf(\SessionHandler::class, $storage->getSaveHandler()); - $storage->setSaveHandler(new \SessionHandler()); - $this->assertInstanceOf(\SessionHandler::class, $storage->getSaveHandler()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); + $storage->setSaveHandler(new SessionHandlerProxy(new NativeFileSessionHandler())); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); + $storage->setSaveHandler(new NativeFileSessionHandler()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); + $storage->setSaveHandler(new SessionHandlerProxy(new NullSessionHandler())); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); $storage->setSaveHandler(new NullSessionHandler()); - $this->assertInstanceOf(\SessionHandlerInterface::class, $storage->getSaveHandler()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy', $storage->getSaveHandler()); } /** @@ -251,12 +218,12 @@ class NativeSessionStorageTest extends TestCase { $storage = $this->getStorage(); - $this->assertNotSame(\PHP_SESSION_ACTIVE, session_status()); + $this->assertFalse($storage->getSaveHandler()->isActive()); $this->assertFalse($storage->isStarted()); session_start(); $this->assertTrue(isset($_SESSION)); - $this->assertSame(\PHP_SESSION_ACTIVE, session_status()); + $this->assertTrue($storage->getSaveHandler()->isActive()); // PHP session might have started, but the storage driver has not, so false is correct here $this->assertFalse($storage->isStarted()); diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/AbstractProxyTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/AbstractProxyTest.php new file mode 100644 index 0000000000..cbb291f19f --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/AbstractProxyTest.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Proxy; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; + +/** + * Test class for AbstractProxy. + * + * @author Drak + */ +class AbstractProxyTest extends TestCase +{ + /** + * @var AbstractProxy + */ + protected $proxy; + + protected function setUp() + { + $this->proxy = $this->getMockForAbstractClass(AbstractProxy::class); + } + + protected function tearDown() + { + $this->proxy = null; + } + + public function testGetSaveHandlerName() + { + $this->assertNull($this->proxy->getSaveHandlerName()); + } + + public function testIsSessionHandlerInterface() + { + $this->assertFalse($this->proxy->isSessionHandlerInterface()); + $sh = new SessionHandlerProxy(new \SessionHandler()); + $this->assertTrue($sh->isSessionHandlerInterface()); + } + + public function testIsWrapper() + { + $this->assertFalse($this->proxy->isWrapper()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testIsActive() + { + $this->assertFalse($this->proxy->isActive()); + session_start(); + $this->assertTrue($this->proxy->isActive()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testName() + { + $this->assertEquals(session_name(), $this->proxy->getName()); + $this->proxy->setName('foo'); + $this->assertEquals('foo', $this->proxy->getName()); + $this->assertEquals(session_name(), $this->proxy->getName()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + * @expectedException \LogicException + */ + public function testNameException() + { + session_start(); + $this->proxy->setName('foo'); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testId() + { + $this->assertEquals(session_id(), $this->proxy->getId()); + $this->proxy->setId('foo'); + $this->assertEquals('foo', $this->proxy->getId()); + $this->assertEquals(session_id(), $this->proxy->getId()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + * @expectedException \LogicException + */ + public function testIdException() + { + session_start(); + $this->proxy->setId('foo'); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php new file mode 100644 index 0000000000..682825356a --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Proxy; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; + +/** + * Tests for SessionHandlerProxy class. + * + * @author Drak + * + * @runTestsInSeparateProcesses + * @preserveGlobalState disabled + */ +class SessionHandlerProxyTest extends TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_Matcher + */ + private $mock; + + /** + * @var SessionHandlerProxy + */ + private $proxy; + + protected function setUp() + { + $this->mock = $this->getMockBuilder('SessionHandlerInterface')->getMock(); + $this->proxy = new SessionHandlerProxy($this->mock); + } + + protected function tearDown() + { + $this->mock = null; + $this->proxy = null; + } + + public function testOpenTrue() + { + $this->mock->expects($this->once()) + ->method('open') + ->will($this->returnValue(true)); + + $this->assertFalse($this->proxy->isActive()); + $this->proxy->open('name', 'id'); + $this->assertFalse($this->proxy->isActive()); + } + + public function testOpenFalse() + { + $this->mock->expects($this->once()) + ->method('open') + ->will($this->returnValue(false)); + + $this->assertFalse($this->proxy->isActive()); + $this->proxy->open('name', 'id'); + $this->assertFalse($this->proxy->isActive()); + } + + public function testClose() + { + $this->mock->expects($this->once()) + ->method('close') + ->will($this->returnValue(true)); + + $this->assertFalse($this->proxy->isActive()); + $this->proxy->close(); + $this->assertFalse($this->proxy->isActive()); + } + + public function testCloseFalse() + { + $this->mock->expects($this->once()) + ->method('close') + ->will($this->returnValue(false)); + + $this->assertFalse($this->proxy->isActive()); + $this->proxy->close(); + $this->assertFalse($this->proxy->isActive()); + } + + public function testRead() + { + $this->mock->expects($this->once()) + ->method('read'); + + $this->proxy->read('id'); + } + + public function testWrite() + { + $this->mock->expects($this->once()) + ->method('write'); + + $this->proxy->write('id', 'data'); + } + + public function testDestroy() + { + $this->mock->expects($this->once()) + ->method('destroy'); + + $this->proxy->destroy('id'); + } + + public function testGc() + { + $this->mock->expects($this->once()) + ->method('gc'); + + $this->proxy->gc(86400); + } +} diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index 6a9f0586f7..985dfb71d7 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -135,6 +135,11 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface $binding = $bindings[$bindingName]; list($bindingValue, $bindingId) = $binding->getValues(); + + if (!$bindingValue instanceof Reference) { + continue; + } + $binding->setValues(array($bindingValue, $bindingId, true)); $args[$p->name] = $bindingValue; diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index 0e524b92d8..4016deb4ab 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -310,6 +310,22 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase { return array(array(ControllerDummy::class), array('$bar')); } + + public function testDoNotBindScalarValueToControllerArgument() + { + $container = new ContainerBuilder(); + $resolver = $container->register('argument_resolver.service')->addArgument(array()); + + $container->register('foo', ArgumentWithoutTypeController::class) + ->setBindings(array('$someArg' => '%foo%')) + ->addTag('controller.service_arguments'); + + $pass = new RegisterControllerArgumentLocatorsPass(); + $pass->process($container); + + $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); + $this->assertEmpty($locator); + } } class RegisterTestController diff --git a/src/Symfony/Component/Routing/RouteCollection.php b/src/Symfony/Component/Routing/RouteCollection.php index f8b18084be..c6229cfde2 100644 --- a/src/Symfony/Component/Routing/RouteCollection.php +++ b/src/Symfony/Component/Routing/RouteCollection.php @@ -128,7 +128,9 @@ class RouteCollection implements \IteratorAggregate, \Countable $this->routes[$name] = $route; } - $this->resources = array_merge($this->resources, $collection->getResources()); + foreach ($collection->getResources() as $resource) { + $this->addResource($resource); + } } /** @@ -262,16 +264,21 @@ class RouteCollection implements \IteratorAggregate, \Countable */ public function getResources() { - return array_unique($this->resources); + return array_values($this->resources); } /** - * Adds a resource for this collection. + * Adds a resource for this collection. If the resource already exists + * it is not added. * * @param ResourceInterface $resource A resource instance */ public function addResource(ResourceInterface $resource) { - $this->resources[] = $resource; + $key = (string) $resource; + + if (!isset($this->resources[$key])) { + $this->resources[$key] = $resource; + } } } diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php b/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php index 5ce2774114..d441ba6ed3 100644 --- a/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php @@ -97,12 +97,18 @@ class NativeSessionTokenStorage implements TokenStorageInterface $this->startSession(); } - $token = isset($_SESSION[$this->namespace][$tokenId]) - ? (string) $_SESSION[$this->namespace][$tokenId] - : null; + if (!isset($_SESSION[$this->namespace][$tokenId])) { + return; + } + + $token = (string) $_SESSION[$this->namespace][$tokenId]; unset($_SESSION[$this->namespace][$tokenId]); + if (!$_SESSION[$this->namespace]) { + unset($_SESSION[$this->namespace]); + } + return $token; }