* */ class Licenses { protected $paths = []; protected $mailMap = []; protected $checkFiles = []; public $authors = []; public function __construct() { $this->licenseText = <<. * */ EOD; $this->licenseTextLegacy = << * */ EOD; $this->licenseTextLegacy = str_replace('@YEAR@', date("Y"), $this->licenseTextLegacy); } /** * @param string|string[] $folder * @param string|bool $gitRoot */ public function exec($folder, $gitRoot = false) { if (is_array($folder)) { foreach ($folder as $f) { $this->exec($f, $gitRoot); } return; } if ($gitRoot !== false && substr($gitRoot, -1) !== '/') { $gitRoot .= '/'; } if (is_file($folder)) { $this->handleFile($folder, $gitRoot); $this->printFilesToCheck(); return; } $excludes = array_map(function ($item) use ($folder) { return $folder . '/' . $item; }, ['vendor', '3rdparty', '.git', 'l10n', 'templates', 'composer']); $iterator = new RecursiveDirectoryIterator($folder, RecursiveDirectoryIterator::SKIP_DOTS); $iterator = new RecursiveCallbackFilterIterator($iterator, function ($item) use ($folder, $excludes) { /** @var SplFileInfo $item */ foreach ($excludes as $exclude) { if (substr($item->getPath(), 0, strlen($exclude)) === $exclude) { return false; } } return true; }); $iterator = new RecursiveIteratorIterator($iterator); $iterator = new RegexIterator($iterator, '/^.+\.php$/i'); foreach ($iterator as $file) { /** @var SplFileInfo $file */ $this->handleFile($file, $gitRoot); } $this->printFilesToCheck(); } public function writeAuthorsFile() { ksort($this->authors); $template = "Nextcloud is written by: @AUTHORS@ With help from many libraries and frameworks including: Open Collaboration Services SabreDAV jQuery … "; $authors = implode(PHP_EOL, array_map(function ($author) { return " - ".$author; }, $this->authors)); $template = str_replace('@AUTHORS@', $authors, $template); file_put_contents(__DIR__.'/../AUTHORS', $template); } public function handleFile($path, $gitRoot) { $source = file_get_contents($path); if ($this->isMITLicensed($source)) { echo "MIT licensed file: $path" . PHP_EOL; return; } $copyrightNotices = $this->getCopyrightNotices($path, $source); $authors = $this->getAuthors($path, $gitRoot); if ($this->isOwnCloudLicensed($source)) { $license = str_replace('@AUTHORS@', $authors, $this->licenseTextLegacy); $this->checkCopyrightState($path, $gitRoot); } else { $license = str_replace('@AUTHORS@', $authors, $this->licenseText); } if ($copyrightNotices === '') { $license = str_replace('@COPYRIGHT@', ' *', $license); } else { $license = str_replace('@COPYRIGHT@', $copyrightNotices, $license); } [$source, $isStrict] = $this->eatOldLicense($source); if ($isStrict) { $source = "getTimestamp(); $buildDir = getcwd(); if ($gitRoot) { chdir($gitRoot); $path = substr($path, strlen($gitRoot)); } $out = shell_exec("git --no-pager blame --line-porcelain $path | sed -n 's/^author-time //p'"); if ($gitRoot) { chdir($buildDir); } $timestampChanges = explode(PHP_EOL, $out); $timestampChanges = array_slice($timestampChanges, 0, count($timestampChanges) - 1); foreach ($timestampChanges as $timestamp) { if ((int)$timestamp < $deadlineTimestamp) { return; } } //all changes after the deadline $this->checkFiles[] = $path; } private function printFilesToCheck() { if (!empty($this->checkFiles)) { print "\n"; print "For following files all lines changed since the Nextcloud fork." . PHP_EOL; print "Please check if these files can be moved over to AGPLv3 or later" . PHP_EOL; print "\n"; foreach ($this->checkFiles as $file) { print $file . PHP_EOL; } print "\n"; } } private function getAuthors($file, $gitRoot) { // only add authors that changed code and not the license header $licenseHeaderEndsAtLine = trim(shell_exec("grep -n '*/' $file | head -n 1 | cut -d ':' -f 1")); $buildDir = getcwd(); if ($gitRoot) { chdir($gitRoot); $file = substr($file, strlen($gitRoot)); } $out = shell_exec("git blame --line-porcelain -L $licenseHeaderEndsAtLine, $file | sed -n 's/^author //p;s/^author-mail //p' | sed 'N;s/\\n/ /' | sort -f | uniq"); if ($gitRoot) { chdir($buildDir); } $authors = explode(PHP_EOL, $out); $authors = array_filter($authors, function ($author) { return !in_array($author, [ '', 'Not Committed Yet ', 'Jenkins for ownCloud ', 'Scrutinizer Auto-Fixer ', ]); }); if ($gitRoot) { $authors = array_map([$this, 'checkCoreMailMap'], $authors); $authors = array_unique($authors); } $authors = array_map(function ($author) { $author = $this->fixInvalidEmail($author); $this->authors[$author] = $author; return " * @author $author"; }, $authors); return implode(PHP_EOL, $authors); } private function checkCoreMailMap($author) { if (empty($this->mailMap)) { $content = file_get_contents(__DIR__ . '/../.mailmap'); $entries = explode("\n", $content); foreach ($entries as $entry) { if (strpos($entry, '> ') === false) { $this->mailMap[$entry] = $entry; } else { [$use, $actual] = explode('> ', $entry); $this->mailMap[$actual] = $use . '>'; } } } if (isset($this->mailMap[$author])) { return $this->mailMap[$author]; } return $author; } private function fixInvalidEmail($author) { preg_match('/<(.*)>/', $author, $mailMatch); if (count($mailMatch) === 2 && !filter_var($mailMatch[1], FILTER_VALIDATE_EMAIL)) { $author = str_replace('<'.$mailMatch[1].'>', '"'.$mailMatch[1].'"', $author); } return $author; } } $licenses = new Licenses; if (isset($argv[1])) { $licenses->exec($argv[1], isset($argv[2]) ? $argv[1] : false); } else { $licenses->exec([ './appinfo', './lib', './templates' ]); $licenses->writeAuthorsFile(); }