diff --git a/.build/license.php b/.build/license.php
new file mode 100644
index 000000000000..6ceb73e7a105
--- /dev/null
+++ b/.build/license.php
@@ -0,0 +1,459 @@
+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|txt|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();
+}
diff --git a/.github/workflows/update-copyright.yml b/.github/workflows/update-copyright.yml
index 425020278bdd..574eb9fe4507 100644
--- a/.github/workflows/update-copyright.yml
+++ b/.github/workflows/update-copyright.yml
@@ -11,3 +11,43 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+ - name: Fetch history
+ run: git fetch --prune --unshallow
+ - name: Set up php
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.1
+ coverage: none
+
+ - name: Run script
+ run: |
+ set -x
+ cd .build
+ php license.php
+ cd ../
+
+ - name: Run script 2
+ run: |
+ set -x
+ cd ../
+ # todo: remove the single branch below and clone master
+ git clone https://github.com/nextcloud/github_helper.git --depth 1 --single-branch --branch enh/noid/fix-exit
+ ls -la
+ cd github_helper/spdx-convertor
+ composer install
+ cd ../
+ cd ../
+ ls -la
+ php github_helper/spdx-convertor/convert.php ./all-in-one
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@v7
+ with:
+ commit-message: Update copyright
+ signoff: true
+ title: Update copyright
+ body: Automated updating copyright headers
+ labels: dependencies, 3. to review
+ milestone: next
+ branch: aio-copyright-update
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 000000000000..3efaf6ad4545
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,5 @@
+
+# Authors