Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 55 additions & 25 deletions core/components/minishop3/config/routes/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
*/

use MiniShop3\Router\Middleware\AuthMiddleware;
use MiniShop3\Router\Middleware\AnyPermissionMiddleware;
use MiniShop3\Router\Middleware\PermissionMiddleware;
use MiniShop3\Services\Category\CategoryProductActionPermissions;
use MiniShop3\Router\HttpStatus;
use MiniShop3\Router\Response;

Expand Down Expand Up @@ -291,25 +293,32 @@
new PermissionMiddleware($modx, 'mssetting_save')
]);

// Customers: permissions align with legacy Customer* processors (#378)
$router->group('/customers', function($router) use ($modx) {
$router->get('', function($params) use ($modx) {
$allParams = array_merge($_GET, $params);

$controller = new \MiniShop3\Controllers\Api\Manager\CustomersController($modx);
return $controller->getList($allParams);
});
}, [
new PermissionMiddleware($modx, 'msorder_list')
]);
// Bulk delete - must be before /{id} route
$router->delete('/bulk', function($params) use ($modx) {
$input = file_get_contents('php://input');
$data = json_decode($input, true) ?: [];

$controller = new \MiniShop3\Controllers\Api\Manager\CustomersController($modx);
return $controller->bulkDelete($data);
});
}, [
new PermissionMiddleware($modx, 'msorder_remove')
]);
$router->get('/{id}', function($params) use ($modx) {
$controller = new \MiniShop3\Controllers\Api\Manager\CustomersController($modx);
return $controller->get($params);
});
}, [
new PermissionMiddleware($modx, 'msorder_view')
]);

$router->put('/{id}', function($params) use ($modx) {
$input = file_get_contents('php://input');
Expand All @@ -318,23 +327,31 @@

$controller = new \MiniShop3\Controllers\Api\Manager\CustomersController($modx);
return $controller->update($data);
});
}, [
new PermissionMiddleware($modx, 'msorder_save')
]);
$router->delete('/{id}', function($params) use ($modx) {
$controller = new \MiniShop3\Controllers\Api\Manager\CustomersController($modx);
return $controller->delete($params);
});
}, [
new PermissionMiddleware($modx, 'msorder_remove')
]);
$router->get('/{id}/addresses', function($params) use ($modx) {
$controller = new \MiniShop3\Controllers\Api\Manager\CustomerAddressesController($modx);
return $controller->getList($params);
});
}, [
new PermissionMiddleware($modx, 'msorder_list')
]);
$router->post('/{id}/addresses', function($params) use ($modx) {
$input = file_get_contents('php://input');
$data = json_decode($input, true) ?: [];
$data['customer_id'] = $params['id'] ?? null;

$controller = new \MiniShop3\Controllers\Api\Manager\CustomerAddressesController($modx);
return $controller->create($data);
});
}, [
new PermissionMiddleware($modx, 'msorder_save')
]);
$router->put('/{id}/addresses/{address_id}', function($params) use ($modx) {
$input = file_get_contents('php://input');
$data = json_decode($input, true) ?: [];
Expand All @@ -343,32 +360,37 @@

$controller = new \MiniShop3\Controllers\Api\Manager\CustomerAddressesController($modx);
return $controller->update($data);
});
}, [
new PermissionMiddleware($modx, 'msorder_save')
]);
$router->delete('/{id}/addresses/{address_id}', function($params) use ($modx) {
$params['address_id'] = $params['address_id'] ?? null;

$controller = new \MiniShop3\Controllers\Api\Manager\CustomerAddressesController($modx);
return $controller->delete($params);
});

}, [
new PermissionMiddleware($modx, 'view_document')
]);
}, [
new PermissionMiddleware($modx, 'msorder_remove')
]);
});

// Category products routes
// Category products: read vs mutate permissions (#378)
$router->group('/categories', function($router) use ($modx) {
// Get products in category
$router->get('/{id}/products', function($params) use ($modx) {
$allParams = array_merge($_GET, $params);

$controller = new \MiniShop3\Controllers\Api\Manager\CategoryProductsController($modx);
return $controller->getList($allParams);
});
}, [
new PermissionMiddleware($modx, 'view_document')
]);
// Get filters configuration
$router->get('/{id}/products/filters', function($params) use ($modx) {
$controller = new \MiniShop3\Controllers\Api\Manager\CategoryProductsController($modx);
return $controller->getFilters($params);
});
}, [
new PermissionMiddleware($modx, 'view_document')
]);
// Sort products (drag-drop)
$router->post('/{id}/products/sort', function($params) use ($modx) {
$input = file_get_contents('php://input');
Expand All @@ -377,7 +399,9 @@

$controller = new \MiniShop3\Controllers\Api\Manager\CategoryProductsController($modx);
return $controller->sort($allParams);
});
}, [
new PermissionMiddleware($modx, 'msproduct_save')
]);
// Bulk delete products
$router->delete('/{id}/products/bulk', function($params) use ($modx) {
$input = file_get_contents('php://input');
Expand All @@ -386,16 +410,23 @@

$controller = new \MiniShop3\Controllers\Api\Manager\CategoryProductsController($modx);
return $controller->bulkDelete($allParams);
});
// Multiple product actions
}, [
new PermissionMiddleware($modx, 'msproduct_delete')
]);
// Multiple product actions — gate any product mutation perm; exact check per method in controller
$router->post('/{id}/products/multiple', function($params) use ($modx) {
$input = file_get_contents('php://input');
$data = json_decode($input, true) ?: [];
$allParams = array_merge($params, $data);

$controller = new \MiniShop3\Controllers\Api\Manager\CategoryProductsController($modx);
return $controller->multiple($allParams);
});
}, [
new AnyPermissionMiddleware(
$modx,
CategoryProductActionPermissions::mutationPermissions()
)
]);
// Toggle product publish status
$router->post('/{id}/products/{productId}/publish', function($params) use ($modx) {
$input = file_get_contents('php://input');
Expand All @@ -404,11 +435,10 @@

$controller = new \MiniShop3\Controllers\Api\Manager\CategoryProductsController($modx);
return $controller->publish($allParams);
});

}, [
new PermissionMiddleware($modx, 'view_document')
]);
}, [
new PermissionMiddleware($modx, 'msproduct_publish')
]);
});

$router->group('/deliveries', function($router) use ($modx) {
$router->get('', function($params) use ($modx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use MiniShop3\Model\msProduct;
use MiniShop3\Router\HttpStatus;
use MiniShop3\Router\Response;
use MiniShop3\Services\Category\CategoryProductActionPermissions;
use MiniShop3\Services\Category\CategoryProductsListService;
use MiniShop3\Services\FilterConfigManager;
use MODX\Revolution\modX;
Expand Down Expand Up @@ -167,6 +168,28 @@ public function multiple(array $params = []): array
return Response::error('Method is required', HttpStatus::BAD_REQUEST)->getData();
}

$access = CategoryProductActionPermissions::evaluate(
$method,
fn(string $permission): bool => $this->modx->hasPermission($permission)
);
if (!$access['allowed']) {
if ($access['reason'] === 'unknown_method') {
$this->modx->log(
modX::LOG_LEVEL_WARN,
"[CategoryProductsController] Unknown method: {$method}"
);
} else {
$this->modx->log(
modX::LOG_LEVEL_WARN,
'[CategoryProductsController] Access denied for permission '
. ($access['permission'] ?? '')
. ' (user id ' . (int)($this->modx->user?->get('id') ?? 0) . ')'
);
}

return Response::error($access['message'], $access['status'])->getData();
}

if (empty($ids) || !is_array($ids)) {
return Response::error('Product IDs array is required', HttpStatus::BAD_REQUEST)->getData();
}
Expand All @@ -191,50 +214,14 @@ public function multiple(array $params = []): array
continue;
}

$result = false;

switch ($method) {
case 'publish':
$product->set('published', 1);
$product->set('publishedon', time());
$product->set('publishedby', $this->modx->user->get('id'));
$result = $product->save();
break;

case 'unpublish':
$product->set('published', 0);
$product->set('publishedon', 0);
$product->set('publishedby', 0);
$result = $product->save();
break;

case 'delete':
$product->set('deleted', 1);
$product->set('deletedon', time());
$product->set('deletedby', $this->modx->user->get('id'));
$result = $product->save();
break;

case 'undelete':
$product->set('deleted', 0);
$product->set('deletedon', 0);
$product->set('deletedby', 0);
$result = $product->save();
break;

case 'show':
$product->set('hidemenu', 0);
$result = $product->save();
break;

case 'hide':
$product->set('hidemenu', 1);
$result = $product->save();
break;

default:
$this->modx->log(modX::LOG_LEVEL_WARN, "[CategoryProductsController] Unknown method: {$method}");
}
$result = match ($method) {
'publish' => $this->applyPublish($product, true),
'unpublish' => $this->applyPublish($product, false),
'delete' => $this->applyDelete($product, true),
'undelete' => $this->applyDelete($product, false),
'show' => $this->applyHideMenu($product, false),
'hide' => $this->applyHideMenu($product, true),
};

if ($result) {
$success++;
Expand Down Expand Up @@ -283,6 +270,10 @@ public function publish(array $params = []): array
return Response::error('Product ID is required', HttpStatus::BAD_REQUEST)->getData();
}

if ($denied = $this->denyWithoutPermission('msproduct_publish')) {
return $denied;
}

$product = $this->modx->getObject(msProduct::class, $productId);

if (!$product) {
Expand All @@ -294,16 +285,7 @@ public function publish(array $params = []): array
$published = $product->get('published') ? 0 : 1;
}

$product->set('published', $published);
if ($published) {
$product->set('publishedon', time());
$product->set('publishedby', $this->modx->user->get('id'));
} else {
$product->set('publishedon', 0);
$product->set('publishedby', 0);
}

if (!$product->save()) {
if (!$this->applyPublish($product, (bool) $published)) {
return Response::error('Failed to update product', HttpStatus::INTERNAL_SERVER_ERROR)->getData();
}

Expand All @@ -313,6 +295,60 @@ public function publish(array $params = []): array
], $published ? 'Product published' : 'Product unpublished')->getData();
}

private function denyWithoutPermission(string $permission): ?array
{
if ($this->modx->hasPermission($permission)) {
return null;
}

$this->modx->log(
modX::LOG_LEVEL_WARN,
'[CategoryProductsController] Access denied for permission ' . $permission
. ' (user id ' . (int)($this->modx->user?->get('id') ?? 0) . ')'
);

return Response::error(
"Access denied. Required permission: {$permission}",
HttpStatus::FORBIDDEN
)->getData();
}

private function applyPublish(msProduct $product, bool $published): bool
{
return $this->setAuditedFlag($product, 'published', 'publishedon', 'publishedby', $published);
}

private function applyDelete(msProduct $product, bool $deleted): bool
{
return $this->setAuditedFlag($product, 'deleted', 'deletedon', 'deletedby', $deleted);
}

private function setAuditedFlag(
msProduct $product,
string $flag,
string $onField,
string $byField,
bool $enabled
): bool {
$product->set($flag, $enabled ? 1 : 0);
if ($enabled) {
$product->set($onField, time());
$product->set($byField, $this->modx->user->get('id'));
} else {
$product->set($onField, 0);
$product->set($byField, 0);
}

return $product->save();
}

private function applyHideMenu(msProduct $product, bool $hidemenu): bool
{
$product->set('hidemenu', $hidemenu ? 1 : 0);

return $product->save();
}

/**
* Get default filters configuration
*
Expand Down
1 change: 1 addition & 0 deletions core/components/minishop3/src/Router/HttpStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class HttpStatus
public const CREATED = 201;
public const BAD_REQUEST = 400;
public const UNAUTHORIZED = 401;
public const FORBIDDEN = 403;
public const NOT_FOUND = 404;
public const METHOD_NOT_ALLOWED = 405;
public const CONFLICT = 409;
Expand Down
Loading