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', 'js', 'node_modules']); $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, '/^.+\.(js|php|md|twig|xml|Dockerfile|Caddyfile|sh|conf|script|cfg|motd|yml|yaml|css)$/i'); foreach ($iterator as $file) { /** @var SplFileInfo $file */ $this->handleFile($file, $gitRoot); } $this->printFilesToCheck(); } public function writeAuthorsFile() { ksort($this->authors); $template = ' # Authors @AUTHORS@ '; $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) { $isPhp = preg_match('/^.+\.php$/i', $path); $isShell = preg_match('/^.+\.sh$/i', $path); $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 === '') { $creator = $this->getCreatorCopyright($path, $gitRoot); $license = str_replace('@COPYRIGHT@', $creator, $license); } else { $license = str_replace('@COPYRIGHT@', $copyrightNotices, $license); } [$source, $isStrict] = $this->eatOldLicense($source); if ($isPhp) { 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 filterAuthors($authors = []) { $authors = array_filter($authors, function ($author) { return !in_array($author, [ '', 'Not Committed Yet ', 'Jenkins for ownCloud ', 'Scrutinizer Auto-Fixer ', ]); }); // Strip out dependabot $authors = array_filter($authors, function ($author) { return strpos($author, 'dependabot') === false; }); return $authors; } private function getCreatorCopyright($file, $gitRoot) { $buildDir = getcwd(); if ($gitRoot) { chdir($gitRoot); $file = substr($file, strlen($gitRoot)); } $year = date('Y'); $blame = shell_exec("git blame --line-porcelain $file | sed -n 's/^author //p;s/^author-mail //p' | sed 'N;s/\\n/ /'"); $authors = explode(PHP_EOL, $blame); if ($gitRoot) { chdir($buildDir); } $authors = $this->filterAuthors($authors); if ($gitRoot) { $authors = array_map([$this, 'checkCoreMailMap'], $authors); $authors = array_unique($authors); } $creator = array_key_exists(0, $authors) ? $this->fixInvalidEmail($authors[0]) : ''; return " * @copyright Copyright (c) $year $creator"; } 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 = $this->filterAuthors($authors); 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([ // '../.github', // Not possible because of workflow restrictions '../app', '../community-containers', '../Containers', '../manual-install', '../nextcloud-aio-helm-chart', '../php', '../tests', '../compose.yaml', '../develop.md', '../docker-ipv6-support.md', '../docker-rootless.md', '../local-instance.md', '../manual-upgrade.md', '../migration.md', '../multiple-instances.md', '../readme.md', '../reverse-proxy.md', ]); $licenses->writeAuthorsFile(); }