* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace JMS\SecurityExtraBundle\Analysis; use Doctrine\Common\Annotations\Reader; use JMS\SecurityExtraBundle\Metadata\Driver\AnnotationDriver; use JMS\SecurityExtraBundle\Metadata\MethodMetadata; use JMS\SecurityExtraBundle\Metadata\ClassMetadata; use JMS\SecurityExtraBundle\Metadata\ServiceMetadata; use Metadata\Driver\DriverChain; use \ReflectionClass; /** * Analyzes a service class including parent classes. The gathered information * is then used to built a proxy class if necessary. * * @author Johannes M. Schmitt */ class ServiceAnalyzer { private $reflection; private $files; private $driver; private $pdepend; private $analyzed; private $hierarchy; private $metadata; public function __construct($class, Reader $reader) { $this->reflection = new ReflectionClass($class); $this->files = array(); $this->hierarchy = array(); $this->driver = new DriverChain(array( new AnnotationDriver($reader), )); $this->analyzed = false; } public function analyze() { if (true === $this->analyzed) { return; } $this->collectFiles(); $this->buildClassHierarchy(); $this->collectServiceMetadata(); if ($this->metadata->isProxyRequired()) { $this->normalizeMetadata(); $this->analyzeControlFlow(); } $this->analyzed = true; } public function getFiles() { if (!$this->analyzed) { throw new \LogicException('Data not yet available, run analyze() first.'); } return $this->files; } public function getMetadata() { if (!$this->analyzed) { throw new \LogicException('Data not yet available, run analyze() first.'); } return $this->metadata; } private function buildClassHierarchy() { $hierarchy = array(); $class = $this->reflection; // add classes while (false !== $class) { $hierarchy[] = $class; $class = $class->getParentClass(); } // add interfaces $addedInterfaces = array(); $newHierarchy = array(); foreach (array_reverse($hierarchy) as $class) { foreach ($class->getInterfaces() as $interface) { if (isset($addedInterfaces[$interface->getName()])) { continue; } $addedInterfaces[$interface->getName()] = true; $newHierarchy[] = $interface; } $newHierarchy[] = $class; } $this->hierarchy = array_reverse($newHierarchy); } private function collectFiles() { $this->files[] = $this->reflection->getFileName(); foreach ($this->reflection->getInterfaces() as $interface) { if (false !== $filename = $interface->getFileName()) { $this->files[] = $filename; } } $parent = $this->reflection; while (false !== $parent = $parent->getParentClass()) { if (false !== $filename = $parent->getFileName()) { $this->files[] = $filename; } } } private function normalizeMetadata() { $secureMethods = array(); foreach ($this->metadata->classMetadata as $class) { if ($class->reflection->isFinal()) { throw new \RuntimeException('Final classes cannot be secured.'); } foreach ($class->methodMetadata as $name => $method) { if ($method->reflection->isStatic() || $method->reflection->isFinal()) { throw new \RuntimeException('Annotations cannot be defined on final, or static methods.'); } if (!isset($secureMethods[$name])) { $this->metadata->addMethodMetadata($method); $secureMethods[$name] = $method; } else if ($method->reflection->isAbstract()) { $secureMethods[$name]->merge($method); } else if (false === $secureMethods[$name]->satisfiesParentSecurityPolicy && $method->reflection->getDeclaringClass()->getName() !== $secureMethods[$name]->reflection->getDeclaringClass()->getName()) { throw new \RuntimeException(sprintf('Unresolved security metadata conflict for method "%s::%s" in "%s". Please copy the respective annotations, and add @SatisfiesParentSecurityPolicy to the child method.', $secureMethods[$name]->reflection->getDeclaringClass()->getName(), $name, $secureMethods[$name]->reflection->getDeclaringClass()->getFileName())); } } } foreach ($secureMethods as $name => $method) { if ($method->reflection->isAbstract()) { $previous = null; $abstractClass = $method->reflection->getDeclaringClass()->getName(); foreach ($this->hierarchy as $refClass) { if ($abstractClass === $fqcn = $refClass->getName()) { $methodMetadata = new MethodMetadata($previous->getName(), $name); $methodMetadata->merge($method); $this->metadata->addMethodMetadata($methodMetadata); continue 2; } if (!$refClass->isInterface() && $this->hasMethod($refClass, $name)) { $previous = $refClass; } } } } } /** * We only perform a very lightweight control flow analysis. If we stumble upon * something suspicous, we will simply break, and require additional metadata * to resolve the situation. * * @throws \RuntimeException * @return void */ private function analyzeControlFlow() { $secureMethods = $this->metadata->methodMetadata; $rootClass = $this->hierarchy[0]; while (true) { foreach ($rootClass->getMethods() as $method) { if (!$this->hasMethod($rootClass, $method->getName())) { continue; } if (!isset($secureMethods[$name = $method->getName()])) { continue; } if ($secureMethods[$name]->reflection->getDeclaringClass()->getName() !== $rootClass->getName()) { throw new \RuntimeException(sprintf( 'You have overridden a secured method "%s::%s" in "%s". ' .'Please copy over the applicable security metadata, and ' .'also add @SatisfiesParentSecurityPolicy.', $secureMethods[$name]->reflection->getDeclaringClass()->getName(), $name, $rootClass->getName() )); } unset($secureMethods[$method->getName()]); } if (null === $rootClass = $rootClass->getParentClass()) { break; } if (0 === count($secureMethods)) { break; } } } private function collectServiceMetadata() { $this->metadata = new ServiceMetadata(); $classMetadata = null; foreach ($this->hierarchy as $reflectionClass) { if (null === $classMetadata) { $classMetadata = new ClassMetadata($reflectionClass->getName()); } if (null !== $aMetadata = $this->driver->loadMetadataForClass($reflectionClass)) { if ($reflectionClass->isInterface()) { $classMetadata->merge($aMetadata); } else { $this->metadata->addClassMetadata($classMetadata); $classMetadata = $aMetadata; } } } $this->metadata->addClassMetadata($classMetadata); } private function hasMethod(\ReflectionClass $class, $name) { if (!$class->hasMethod($name)) { return false; } return $class->getName() === $class->getMethod($name)->getDeclaringClass()->getName(); } }