Skip to content
Open
32 changes: 29 additions & 3 deletions css/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1082,14 +1082,18 @@ input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:fo
margin-bottom: 4px;
margin-right: 4px;
}
.package .details #add-maintainer {
.package .details #add-maintainer{
margin-top: 3px;
margin-bottom: 7px;
float: right;
font-size: 34px;
}
.package .details #transfer-package {
margin-top: 7px;
margin-bottom: 7px;
margin-right: 4px;
font-size: 34px;
}
.package .details #remove-maintainer {
float: right;
font-size: 35px;
margin-top: 5px;
}
Expand Down Expand Up @@ -1884,3 +1888,25 @@ body {
}
}
}

#transfer-package-form {
.maintainers-collection-wrapper {
margin-bottom: 15px;

.maintainers-list {
list-style: none;
padding: 0;

li {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;

.btn-danger {
padding: 5px 10px;
}
}
}
}
}
49 changes: 45 additions & 4 deletions js/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@ const init = function ($) {
var versionCache = {},
ongoingRequest = false;

$('#add-maintainer').on('click', function (e) {
const togglePackageForm = function (selector) {
$('#remove-maintainer-form').addClass('hidden');
$('#add-maintainer-form').removeClass('hidden');
$('#add-maintainer-form').addClass('hidden');
$('#transfer-package-form').addClass('hidden');
$(selector).removeClass('hidden');
}

$('#add-maintainer').on('click', function (e) {
togglePackageForm('#add-maintainer-form');
e.preventDefault();
});
$('#remove-maintainer').on('click', function (e) {
$('#add-maintainer-form').addClass('hidden');
$('#remove-maintainer-form').removeClass('hidden');
togglePackageForm('#remove-maintainer-form');
e.preventDefault();
});
$('#transfer-package').on('click', function (e) {
togglePackageForm('#transfer-package-form');
e.preventDefault();
});

Expand Down Expand Up @@ -227,6 +236,38 @@ const init = function ($) {
$(versionsList).css('max-height', 'inherit');
});
}

// Handle add/remove buttons for transfer package form
$('.add-maintainer-item').on('click', function (e) {
e.preventDefault();

var list = $('.maintainers-list');
var prototype = list.data('prototype');
var index = list.find('li').length + 1;

var newForm = prototype.replace(/__name__/g, index);
var newItem = $('<li></li>').append(newForm);
addMaintainerRemoveButton(newItem);
list.append(newItem);
});

$('.maintainers-list').find('li').each(function(index) {
addMaintainerRemoveButton($(this));
});

function addMaintainerRemoveButton(item) {
var removeButton = $('<button type="button" class="btn btn-danger btn-sm"><i class="glyphicon glyphicon-remove"></i></button>');
removeButton.on('click', function(e) {
e.preventDefault();

if ($('.maintainers-list').find('li').length === 1) {
return;
}

item.remove();
});
item.append(removeButton);
}
};

if (document.querySelector('#view-package-page')) {
Expand Down
20 changes: 3 additions & 17 deletions src/Command/TransferOwnershipCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use App\Entity\AuditRecord;
use App\Entity\Package;
use App\Entity\User;
use App\Model\PackageManager;
use App\Util\DoctrineTrait;
use Composer\Console\Input\InputOption;
use Doctrine\Persistence\ManagerRegistry;
Expand All @@ -30,6 +31,7 @@ class TransferOwnershipCommand extends Command

public function __construct(
private readonly ManagerRegistry $doctrine,
private readonly PackageManager $packageManager,
)
{
parent::__construct();
Expand Down Expand Up @@ -165,24 +167,8 @@ private function outputPackageTable(OutputInterface $output, array $packages, ar
*/
private function transferOwnership(array $packages, array $maintainers): void
{
$normalizedMaintainers = array_values(array_map(fn (User $user) => $user->getId(), $maintainers));
sort($normalizedMaintainers, SORT_NUMERIC);

foreach ($packages as $package) {
$oldMaintainers = $package->getMaintainers()->toArray();

$normalizedOldMaintainers = array_values(array_map(fn (User $user) => $user->getId(), $oldMaintainers));
sort($normalizedOldMaintainers, SORT_NUMERIC);
if ($normalizedMaintainers === $normalizedOldMaintainers) {
continue;
}

$package->getMaintainers()->clear();
foreach ($maintainers as $maintainer) {
$package->addMaintainer($maintainer);
}

$this->doctrine->getManager()->persist(AuditRecord::packageTransferred($package, null, $oldMaintainers, array_values($maintainers)));
$this->packageManager->transferPackage($package, $maintainers);
}

$this->doctrine->getManager()->flush();
Expand Down
2 changes: 1 addition & 1 deletion src/Controller/FeedController.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public function extensionReleasesAction(Request $req): Response
return $this->buildResponse($req, $feed);
}

#[Route(path: '/package.{package}.{_format}', name: 'feed_package', requirements: ['_format' => '(rss|atom)', 'package' => '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+'], methods: ['GET'])]
#[Route(path: '/package.{package}.{_format}', name: 'feed_package', requirements: ['_format' => '(rss|atom)', 'package' => Package::PACKAGE_NAME_REGEX], methods: ['GET'])]
public function packageAction(Request $req, string $package): Response
{
$repo = $this->doctrine->getRepository(Version::class);
Expand Down
58 changes: 54 additions & 4 deletions src/Controller/PackageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
use App\Entity\Vendor;
use App\Entity\Version;
use App\Form\Model\MaintainerRequest;
use App\Form\Model\TransferPackageRequest;
use App\Form\Type\AbandonedType;
use App\Form\Type\AddMaintainerRequestType;
use App\Form\Type\PackageType;
use App\Form\Type\RemoveMaintainerRequestType;
use App\Form\Type\TransferPackageRequestType;
use App\Model\DownloadManager;
use App\Model\FavoriteManager;
use App\Model\PackageManager;
Expand Down Expand Up @@ -692,6 +694,7 @@ public function viewPackageAction(Request $req, string $name, CsrfTokenManagerIn

$data['addMaintainerForm'] = $this->createAddMaintainerForm($package)->createView();
$data['removeMaintainerForm'] = $this->createRemoveMaintainerForm($package)->createView();
$data['transferPackageForm'] = $this->createTransferPackageForm($package)->createView();
$data['deleteForm'] = $this->createDeletePackageForm($package)->createView();
} else {
$data['hasVersionSecurityAdvisories'] = [];
Expand Down Expand Up @@ -827,7 +830,7 @@ public function deletePackageVersionAction(Request $req, int $versionId): Respon
return new Response('', 204);
}

#[Route(path: '/packages/{name}', name: 'update_package', requirements: ['name' => '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+'], defaults: ['_format' => 'json'], methods: ['PUT'])]
#[Route(path: '/packages/{name}', name: 'update_package', requirements: ['name' => Package::PACKAGE_NAME_REGEX], defaults: ['_format' => 'json'], methods: ['PUT'])]
public function updatePackageAction(Request $req, string $name, #[CurrentUser] User $user): Response
{
try {
Expand Down Expand Up @@ -879,7 +882,7 @@ public function updatePackageAction(Request $req, string $name, #[CurrentUser] U
return new JsonResponse(['status' => 'error', 'message' => 'Package was already updated in the last 24 hours'], 404);
}

#[Route(path: '/packages/{name}', name: 'delete_package', requirements: ['name' => '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+'], methods: ['DELETE'])]
#[Route(path: '/packages/{name}', name: 'delete_package', requirements: ['name' => Package::PACKAGE_NAME_REGEX], methods: ['DELETE'])]
public function deletePackageAction(Request $req, string $name): Response
{
$package = $this->getPartialPackageWithVersions($req, $name);
Expand All @@ -904,7 +907,7 @@ public function deletePackageAction(Request $req, string $name): Response
return new Response('Invalid form input', 400);
}

#[Route(path: '/packages/{name:package}/maintainers/', name: 'add_maintainer', requirements: ['name' => '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+'])]
#[Route(path: '/packages/{name:package}/maintainers/', name: 'add_maintainer', requirements: ['name' => Package::PACKAGE_NAME_REGEX])]
public function createMaintainerAction(Request $req, #[MapEntity] Package $package, #[CurrentUser] User $user, LoggerInterface $logger): RedirectResponse
{
$this->denyAccessUnlessGranted(PackageActions::AddMaintainer->value, $package);
Expand Down Expand Up @@ -942,7 +945,7 @@ public function createMaintainerAction(Request $req, #[MapEntity] Package $packa
return $this->redirectToRoute('view_package', ['name' => $package->getName()]);
}

#[Route(path: '/packages/{name:package}/maintainers/delete', name: 'remove_maintainer', requirements: ['name' => '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+'])]
#[Route(path: '/packages/{name:package}/maintainers/delete', name: 'remove_maintainer', requirements: ['name' => Package::PACKAGE_NAME_REGEX])]
public function removeMaintainerAction(Request $req, #[MapEntity] Package $package, #[CurrentUser] User $user, LoggerInterface $logger): Response
{
$this->denyAccessUnlessGranted(PackageActions::RemoveMaintainer->value, $package);
Expand Down Expand Up @@ -986,6 +989,42 @@ public function removeMaintainerAction(Request $req, #[MapEntity] Package $packa
]);
}


#[Route(path: '/packages/{name:package}/transfer/', name: 'transfer_package', requirements: ['name' => Package::PACKAGE_NAME_REGEX])]
public function transferPackageAction(Request $req, #[MapEntity] Package $package, #[CurrentUser] User $user, LoggerInterface $logger): RedirectResponse
{
$this->denyAccessUnlessGranted(PackageActions::TransferPackage->value, $package);

$form = $this->createTransferPackageForm($package);
$form->handleRequest($req);

if ($form->isSubmitted() && $form->isValid()) {
try {
$newMaintainers = $form->getData()->getMaintainers()->toArray();
$result = $this->packageManager->transferPackage($package, $newMaintainers);
$this->getEM()->flush();

if ($result) {
$usernames = array_map(fn (User $user) => $user->getUsername(), $newMaintainers);
$this->addFlash('success', sprintf('Package has been transferred to %s', implode(', ', $usernames)));
} else {
$this->addFlash('warning', 'Package maintainers are identical and have not been changed');
}

return $this->redirectToRoute('view_package', ['name' => $package->getName()]);
} catch (\Exception $e) {
$logger->critical($e->getMessage(), ['exception', $e]);
$this->addFlash('error', 'The package could not be transferred.');
}
} elseif (!$form->isValid()) {
foreach ($form->getErrors(true, true) as $error) {
$this->addFlash('error', $error->getMessage());
}
}

return $this->redirectToRoute('view_package', ['name' => $package->getName()]);
}

#[Route(path: '/packages/{name:package}/edit', name: 'edit_package', requirements: ['name' => '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?'])]
public function editAction(Request $req, #[MapEntity] Package $package, #[CurrentUser] ?User $user = null): Response
{
Expand Down Expand Up @@ -1623,6 +1662,17 @@ private function createRemoveMaintainerForm(Package $package): FormInterface
]);
}

/**
* @return FormInterface<TransferPackageRequest>
*/
private function createTransferPackageForm(Package $package): FormInterface
{
$transferRequest = new TransferPackageRequest();
$transferRequest->setMaintainers(clone $package->getMaintainers());

return $this->createForm(TransferPackageRequestType::class, $transferRequest);
}

/**
* @return FormInterface<array{}>
*/
Expand Down
2 changes: 2 additions & 0 deletions src/Entity/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ class Package
public const AUTO_MANUAL_HOOK = 1;
public const AUTO_GITHUB_HOOK = 2;

public const string PACKAGE_NAME_REGEX = '[a-zA-Z0-9](?:[_.-]?[a-zA-Z0-9]+)*/[a-zA-Z0-9](?:[_.-]?[a-zA-Z0-9]+)*';

#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue(strategy: 'AUTO')]
Expand Down
46 changes: 46 additions & 0 deletions src/Form/Model/TransferPackageRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types=1);

/*
* This file is part of Packagist.
*
* (c) Jordi Boggiano <[email protected]>
* Nils Adermann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Form\Model;

use App\Entity\User;
use App\Validator\Constraints\TransferPackageValidMaintainersList;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

class TransferPackageRequest
{
/** @var Collection<int, User> */
#[TransferPackageValidMaintainersList]
private Collection $maintainers;

public function __construct()
{
$this->maintainers = new ArrayCollection();
}

/**
* @return Collection<int, User>
*/
public function getMaintainers(): Collection
{
return $this->maintainers;
}

/**
* @param Collection<int, User> $maintainers
*/
public function setMaintainers(Collection $maintainers): void
{
$this->maintainers = $maintainers;
}
}
Loading