<?php
/**
 * @author Jörn Friedrich Dreyer <jfd@butonic.de>
 *
 * @copyright Copyright (c) 2018, ownCloud GmbH
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 */

namespace OC\Core\Command\Security;

use OC\AppFramework\App;
use OC\Core\Command\Base;
use OC\Route\Route;
use OC\Route\Router;
use OCP\Route\IRouter;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class ListRoutes extends Base {
	/**
	 * @var Router
	 */
	protected $router;

	public function __construct(IRouter $router) {
		$this->router = $router;
		parent::__construct();
	}

	protected function configure() {
		$this
			->setName('security:routes')
			->setDescription('List used routes.')
			->addOption('with-details', 'd', InputOption::VALUE_NONE);
		parent::configure();
	}

	protected function execute(InputInterface $input, OutputInterface $output): int {
		$outputType = $input->getOption('output');

		\OC_App::loadApps();
		$this->router->loadRoutes();

		$rows = [];

		if ($input->getOption('with-details')) {
			$headers = [
				'Path',
				'Methods',
				'Controller',
				'Annotations',
			];
			/** @var Route[] $collections */
			$collections = [];
			foreach ($this->router->getCollections() as $c) {
				$new = $c->all();
				$collections = $collections + $new;
			}

			foreach ($collections as $name => $route) {
				$c = $this->buildController($name);
				$rows[] = \array_merge([
					'path' => $route->getPath(),
					'methods' => $route->getMethods()
				], $c);
			}
		} else {
			$headers = [
				'Path',
				'Methods'
			];
			foreach ($this->router->getCollections() as $routeCollection) {
				foreach ($routeCollection as $route) {
					$path = $route->getPath();
					if (isset($rows[$path])) {
						$rows[$path]['methods'] = \array_unique(\array_merge($rows[$path]['methods'], $route->getMethods()));
					} else {
						$rows[$path] = [
							'path' => $path,
							'methods' => $route->getMethods()
						];
					}
					\sort($rows[$path]['methods']);
				}
			}
		}
		\usort($rows, function ($a, $b) {
			return \strcmp($a['path'], $b['path']);
		});
		$rows = \array_map(function ($row) {
			$row['methods'] = \implode(',', $row['methods']);
			return $row;
		}, $rows);

		if ($outputType === self::OUTPUT_FORMAT_JSON) {
			$output->write(\json_encode($rows));
		} elseif ($outputType === self::OUTPUT_FORMAT_JSON_PRETTY) {
			$output->writeln(\json_encode($rows, JSON_PRETTY_PRINT));
		} else {
			$table = new Table($output);
			$table->setHeaders($headers);

			$table->addRows($rows);
			$table->render();
		}
		return 0;
	}

	private function buildController($name) {
		$parts = \explode('.', $name);
		if (\count($parts) === 4 && $parts[0] === 'ocs') {
			\array_shift($parts);
		}
		if (\count($parts) !== 3) {
			return [
				'controllerClass' => '*** not controller based ***'
			];
		}
		$appName = $parts[0];
		$controllerName = $parts[1];
		$method = $parts[2];
		$reflection = $this->buildReflection($appName, $controllerName, $method);
		if ($reflection === null) {
			return [
				'controllerClass' => '*** controller not resolvable ***'
			];
		}
		$docs = $reflection->getDocComment();

		// extract everything prefixed by @ and first letter uppercase
		\preg_match_all('/@([A-Z]\w+)/', $docs, $matches);
		$annotations = $matches[1];

		return [
			'controllerClass' => $reflection->getDeclaringClass()->getName() . '::' . $reflection->getName(),
			'annotations' => \implode(',', $annotations),
		];
	}

	/**
	 * @param string $appName
	 * @param string $controllerName
	 * @param string $method
	 * @return null|\ReflectionMethod
	 */
	private function buildReflection($appName, $controllerName, $method) {
		foreach ($this->listControllerNames($appName, $controllerName) as $controllerName) {
			foreach ($this->listMethodNames($method) as $m) {
				try {
					$reflection = new \ReflectionMethod($controllerName, $m);
					return $reflection;
				} catch (\ReflectionException $ex) {
				}
			}
		}
		return null;
	}

	/**
	 * @param string $appName
	 * @param string $controllerName
	 * @return \Generator | string[]
	 */
	private function listControllerNames($appName, $controllerName) {
		foreach ([App::buildAppNamespace($appName), App::buildAppNamespace($appName, 'OC\\')] as $appNameSpace) {
			foreach (['\\Controller\\', '\\Controllers\\'] as $namespace) {
				yield $appNameSpace . $namespace . $controllerName;
				yield $appNameSpace . $namespace . \ucfirst(\strtolower($controllerName));
				yield $appNameSpace . $namespace . $controllerName . 'Controller';
				yield $appNameSpace . $namespace . \ucfirst(\strtolower($controllerName)) . 'Controller';
				$controllerName = \implode('', \array_map(function ($word) {
					return \ucfirst($word);
				}, \explode('_', $controllerName)));
				yield $appNameSpace . $namespace . $controllerName;
				yield $appNameSpace . $namespace . \ucfirst(\strtolower($controllerName));
				yield $appNameSpace . $namespace . $controllerName . 'Controller';
				yield $appNameSpace . $namespace . \ucfirst(\strtolower($controllerName)) . 'Controller';
			}
		}
	}

	/**
	 * @param string $method
	 * @return \Generator | string[]
	 */
	private function listMethodNames($method) {
		yield $method;
		yield \implode('', \explode('_', $method));
		foreach (['post', 'put'] as $verb) {
			if (\substr($method, -\strlen($verb)) == $verb) {
				yield \substr($method, 0, -\strlen($verb));
				yield \implode('', \explode('_', \substr($method, 0, -\strlen($verb))));
			}
		}
	}
}
