577 lines
18 KiB
PHP
577 lines
18 KiB
PHP
<?php
|
|
|
|
declare (strict_types=1);
|
|
/**
|
|
* SPDX-FileCopyrightText: 2017 F7cloud GmbH and F7cloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\GroupFolders\Controller;
|
|
|
|
use OC\AppFramework\OCS\V1Response;
|
|
use OCA\GroupFolders\Attribute\RequireGroupFolderAdmin;
|
|
use OCA\GroupFolders\Folder\FolderManager;
|
|
use OCA\GroupFolders\Folder\FolderWithMappingsAndCache;
|
|
use OCA\GroupFolders\Mount\FolderStorageManager;
|
|
use OCA\GroupFolders\Mount\MountProvider;
|
|
use OCA\GroupFolders\ResponseDefinitions;
|
|
use OCA\GroupFolders\Service\DelegationService;
|
|
use OCA\GroupFolders\Service\FoldersFilter;
|
|
use OCP\AppFramework\Http;
|
|
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
|
|
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
|
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
|
|
use OCP\AppFramework\Http\DataResponse;
|
|
use OCP\AppFramework\OCS\OCSBadRequestException;
|
|
use OCP\AppFramework\OCS\OCSForbiddenException;
|
|
use OCP\AppFramework\OCS\OCSNotFoundException;
|
|
use OCP\AppFramework\OCSController;
|
|
use OCP\Files\Folder;
|
|
use OCP\Files\IRootFolder;
|
|
use OCP\IGroupManager;
|
|
use OCP\IRequest;
|
|
use OCP\IUser;
|
|
use OCP\IUserSession;
|
|
|
|
/**
|
|
* @psalm-import-type GroupFoldersGroup from ResponseDefinitions
|
|
* @psalm-import-type GroupFoldersCircle from ResponseDefinitions
|
|
* @psalm-import-type GroupFoldersUser from ResponseDefinitions
|
|
* @psalm-import-type GroupFoldersFolder from ResponseDefinitions
|
|
*/
|
|
class FolderController extends OCSController {
|
|
private readonly ?IUser $user;
|
|
|
|
public function __construct(
|
|
string $AppName,
|
|
IRequest $request,
|
|
private readonly FolderManager $manager,
|
|
private readonly MountProvider $mountProvider,
|
|
private readonly IRootFolder $rootFolder,
|
|
IUserSession $userSession,
|
|
private readonly FoldersFilter $foldersFilter,
|
|
private readonly DelegationService $delegationService,
|
|
private readonly IGroupManager $groupManager,
|
|
private readonly FolderStorageManager $folderStorageManager,
|
|
) {
|
|
parent::__construct($AppName, $request);
|
|
$this->user = $userSession->getUser();
|
|
|
|
$this->registerResponder('xml', fn (DataResponse $data): V1Response => $this->buildOCSResponseXML('xml', $data));
|
|
}
|
|
|
|
/**
|
|
* Regular users can access their own folders, but they only get to see the permission for their own groups
|
|
*
|
|
* @param GroupFoldersFolder $folder
|
|
* @return null|GroupFoldersFolder
|
|
*/
|
|
private function filterNonAdminFolder(array $folder): ?array {
|
|
if ($this->user === null) {
|
|
return null;
|
|
}
|
|
|
|
$userGroups = $this->groupManager->getUserGroupIds($this->user);
|
|
$folder['groups'] = array_filter($folder['groups'], static fn (string $group): bool => in_array($group, $userGroups, true), ARRAY_FILTER_USE_KEY);
|
|
$folder['group_details'] = array_filter($folder['group_details'], static fn (string $group): bool => in_array($group, $userGroups, true), ARRAY_FILTER_USE_KEY);
|
|
if ($folder['groups'] !== []) {
|
|
return $folder;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return GroupFoldersFolder
|
|
*/
|
|
private function formatFolder(FolderWithMappingsAndCache $folder): array {
|
|
return [
|
|
'id' => $folder->id,
|
|
'mount_point' => $folder->mountPoint,
|
|
'quota' => $folder->quota,
|
|
'acl' => $folder->acl,
|
|
'size' => $folder->rootCacheEntry->getSize(),
|
|
'groups' => array_map(fn (array $group): int => $group['permissions'], $folder->groups),
|
|
'group_details' => $folder->groups,
|
|
'manage' => $folder->manage,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Gets all Groupfolders
|
|
*
|
|
* @param bool $applicable Filter by applicable groups
|
|
* @param non-negative-int $offset Number of items to skip.
|
|
* @param ?positive-int $limit Number of items to return.
|
|
* @param 'mount_point'|'quota'|'groups'|'acl' $orderBy The key to order by
|
|
* @param 'asc'|'desc' $order Sort ascending or descending
|
|
* @return DataResponse<Http::STATUS_OK, array<string, GroupFoldersFolder>, array{}>
|
|
* @throws OCSNotFoundException Storage not found
|
|
* @throws OCSBadRequestException Wrong limit used
|
|
*
|
|
* 200: Groupfolders returned
|
|
*/
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'GET', url: '/folders')]
|
|
public function getFolders(bool $applicable = false, int $offset = 0, ?int $limit = null, string $orderBy = 'mount_point', string $order = 'asc'): DataResponse {
|
|
/** @psalm-suppress DocblockTypeContradiction */
|
|
if ($limit !== null && $limit <= 0) {
|
|
throw new OCSBadRequestException('The limit must be greater than 0.');
|
|
}
|
|
|
|
/** @psalm-suppress DocblockTypeContradiction */
|
|
if (!in_array($orderBy, ['mount_point', 'quota', 'groups', 'acl'], true)) {
|
|
throw new OCSBadRequestException('The orderBy is not allowed.');
|
|
}
|
|
|
|
/** @psalm-suppress DocblockTypeContradiction */
|
|
if (!in_array($order, ['asc', 'desc'], true)) {
|
|
throw new OCSBadRequestException('The order is not allowed.');
|
|
}
|
|
|
|
$storageId = $this->getRootFolderStorageId();
|
|
if ($storageId === null) {
|
|
throw new OCSNotFoundException();
|
|
}
|
|
|
|
$folders = [];
|
|
$i = 0;
|
|
foreach ($this->manager->getAllFoldersWithSize($offset, $limit, $orderBy, $order) as $id => $folder) {
|
|
// Make them string-indexed for OpenAPI JSON output
|
|
// JavaScript doesn't preserve JSON object key orders, so we need to manually add this information.
|
|
$folders[(string)$id] = array_merge($this->formatFolder($folder), [
|
|
'sortIndex' => $offset + $i++,
|
|
]);
|
|
}
|
|
|
|
$isAdmin = $this->delegationService->isAdminF7cloud() || $this->delegationService->isDelegatedAdmin();
|
|
if ($isAdmin && !$applicable) {
|
|
return new DataResponse($folders);
|
|
}
|
|
|
|
if ($this->delegationService->hasOnlyApiAccess()) {
|
|
$folders = $this->foldersFilter->getForApiUser($folders);
|
|
}
|
|
|
|
if ($applicable || !$this->delegationService->hasApiAccess()) {
|
|
$folders = array_filter(array_map($this->filterNonAdminFolder(...), $folders));
|
|
}
|
|
|
|
return new DataResponse($folders);
|
|
}
|
|
|
|
/**
|
|
* Gets a Groupfolder by ID
|
|
*
|
|
* @param int $id ID of the Groupfolder
|
|
* @return DataResponse<Http::STATUS_OK, GroupFoldersFolder, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
*
|
|
* 200: Groupfolder returned
|
|
*/
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'GET', url: '/folders/{id}')]
|
|
public function getFolder(int $id): DataResponse {
|
|
$storageId = $this->getRootFolderStorageId();
|
|
if ($storageId === null) {
|
|
throw new OCSNotFoundException();
|
|
}
|
|
|
|
$folder = $this->checkedGetFolder($id);
|
|
$folder = $this->formatFolder($folder);
|
|
|
|
if (!$this->delegationService->hasApiAccess()) {
|
|
$folder = $this->filterNonAdminFolder($folder);
|
|
if ($folder === null) {
|
|
throw new OCSNotFoundException();
|
|
}
|
|
}
|
|
|
|
return new DataResponse($folder);
|
|
}
|
|
|
|
/**
|
|
* @throws OCSNotFoundException
|
|
*/
|
|
private function checkedGetFolder(int $id): FolderWithMappingsAndCache {
|
|
$folder = $this->manager->getFolder($id);
|
|
if ($folder === null) {
|
|
throw new OCSNotFoundException('Groupfolder not found');
|
|
}
|
|
|
|
return $folder;
|
|
}
|
|
|
|
private function checkMountPointExists(string $mountpoint): ?DataResponse {
|
|
$storageId = $this->getRootFolderStorageId();
|
|
if ($storageId === null) {
|
|
throw new OCSNotFoundException('Groupfolder not found');
|
|
}
|
|
|
|
$folders = $this->manager->getAllFolders();
|
|
foreach ($folders as $folder) {
|
|
if ($folder->mountPoint === $mountpoint) {
|
|
throw new OCSBadRequestException('Mount point already exists');
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function getRootFolderStorageId(): ?int {
|
|
return $this->rootFolder->getMountPoint()->getNumericStorageId();
|
|
}
|
|
|
|
/**
|
|
* Add a new Groupfolder
|
|
*
|
|
* @param string $mountpoint Mountpoint of the new Groupfolder
|
|
* @param ?string $bucket Overwrite the object store bucket to use for the folder
|
|
* @return DataResponse<Http::STATUS_OK, GroupFoldersFolder, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
* @throws OCSBadRequestException Folder already exists
|
|
*
|
|
* 200: Groupfolder added successfully
|
|
*/
|
|
#[PasswordConfirmationRequired]
|
|
#[RequireGroupFolderAdmin]
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'POST', url: '/folders')]
|
|
public function addFolder(string $mountpoint, ?string $bucket = null): DataResponse {
|
|
$storageId = $this->rootFolder->getMountPoint()->getNumericStorageId();
|
|
if ($storageId === null) {
|
|
throw new OCSNotFoundException();
|
|
}
|
|
|
|
$this->checkMountPointExists(trim($mountpoint));
|
|
|
|
$options = [];
|
|
if ($bucket) {
|
|
$options['bucket'] = $bucket;
|
|
}
|
|
|
|
$id = $this->manager->createFolder(trim($mountpoint), $options);
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
return new DataResponse($this->formatFolder($folder));
|
|
}
|
|
|
|
/**
|
|
* Remove a Groupfolder
|
|
*
|
|
* @param int $id ID of the Groupfolder
|
|
* @return DataResponse<Http::STATUS_OK, array{success: true}, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
*
|
|
* 200: Groupfolder removed successfully
|
|
*/
|
|
#[PasswordConfirmationRequired]
|
|
#[RequireGroupFolderAdmin]
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'DELETE', url: '/folders/{id}')]
|
|
public function removeFolder(int $id): DataResponse {
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
$this->folderStorageManager->deleteStoragesForFolder($folder);
|
|
$this->manager->removeFolder($id);
|
|
|
|
return new DataResponse(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* Set the mount point of a Groupfolder
|
|
*
|
|
* @param int $id ID of the Groupfolder
|
|
* @param string $mountPoint New mount point path
|
|
* @return DataResponse<Http::STATUS_OK, array{success: true, folder: GroupFoldersFolder}, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
* @throws OCSBadRequestException Mount point already exists
|
|
*
|
|
* 200: Mount point changed successfully
|
|
*/
|
|
#[PasswordConfirmationRequired]
|
|
#[RequireGroupFolderAdmin]
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'PUT', url: '/folders/{id}')]
|
|
public function setMountPoint(int $id, string $mountPoint): DataResponse {
|
|
$this->checkMountPointExists(trim($mountPoint));
|
|
|
|
$this->manager->renameFolder($id, trim($mountPoint));
|
|
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]);
|
|
}
|
|
|
|
/**
|
|
* Add access of a group for a Groupfolder
|
|
*
|
|
* @param int $id ID of the Groupfolder
|
|
* @param string $group Group to add access for
|
|
* @return DataResponse<Http::STATUS_OK, array{success: true, folder: GroupFoldersFolder}, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
*
|
|
* 200: Group access added successfully
|
|
*/
|
|
#[PasswordConfirmationRequired]
|
|
#[RequireGroupFolderAdmin]
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'POST', url: '/folders/{id}/groups')]
|
|
public function addGroup(int $id, string $group): DataResponse {
|
|
$this->checkedGetFolder($id);
|
|
|
|
$this->manager->addApplicableGroup($id, $group);
|
|
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]);
|
|
}
|
|
|
|
/**
|
|
* Remove access of a group from a Groupfolder
|
|
*
|
|
* @param int $id ID of the Groupfolder
|
|
* @param string $group Group to remove access from
|
|
* @return DataResponse<Http::STATUS_OK, array{success: true, folder: GroupFoldersFolder}, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
*
|
|
* 200: Group access removed successfully
|
|
*/
|
|
#[PasswordConfirmationRequired]
|
|
#[RequireGroupFolderAdmin]
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'DELETE', url: '/folders/{id}/groups/{group}', requirements: ['group' => '.+'])]
|
|
public function removeGroup(int $id, string $group): DataResponse {
|
|
$this->checkedGetFolder($id);
|
|
|
|
$this->manager->removeApplicableGroup($id, $group);
|
|
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]);
|
|
}
|
|
|
|
/**
|
|
* Set the permissions of a group for a Groupfolder
|
|
*
|
|
* @param int $id ID of the Groupfolder
|
|
* @param string $group Group for which the permissions will be set
|
|
* @param int $permissions New permissions
|
|
* @return DataResponse<Http::STATUS_OK, array{success: true, folder: GroupFoldersFolder}, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
*
|
|
* 200: Permissions updated successfully
|
|
*/
|
|
#[PasswordConfirmationRequired]
|
|
#[RequireGroupFolderAdmin]
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'POST', url: '/folders/{id}/groups/{group}', requirements: ['group' => '.+'])]
|
|
public function setPermissions(int $id, string $group, int $permissions): DataResponse {
|
|
$this->checkedGetFolder($id);
|
|
|
|
$this->manager->setGroupPermissions($id, $group, $permissions);
|
|
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]);
|
|
}
|
|
|
|
/**
|
|
* Updates an ACL mapping
|
|
*
|
|
* @param int $id ID of the Groupfolder
|
|
* @param string $mappingType Type of the ACL mapping
|
|
* @param string $mappingId ID of the ACL mapping
|
|
* @param bool $manageAcl Whether to enable or disable the ACL mapping
|
|
* @return DataResponse<Http::STATUS_OK, array{success: true, folder: GroupFoldersFolder}, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
*
|
|
* 200: ACL mapping updated successfully
|
|
*/
|
|
#[PasswordConfirmationRequired]
|
|
#[RequireGroupFolderAdmin]
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'POST', url: '/folders/{id}/manageACL')]
|
|
public function setManageACL(int $id, string $mappingType, string $mappingId, bool $manageAcl): DataResponse {
|
|
$this->checkedGetFolder($id);
|
|
|
|
$this->manager->setManageACL($id, $mappingType, $mappingId, $manageAcl);
|
|
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]);
|
|
}
|
|
|
|
/**
|
|
* Set a new quota for a Groupfolder
|
|
*
|
|
* @param int $id ID of the Groupfolder
|
|
* @param int $quota New quota in bytes
|
|
* @return DataResponse<Http::STATUS_OK, array{success: true, folder: GroupFoldersFolder}, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
*
|
|
* 200: New quota set successfully
|
|
*/
|
|
#[PasswordConfirmationRequired]
|
|
#[RequireGroupFolderAdmin]
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'POST', url: '/folders/{id}/quota')]
|
|
public function setQuota(int $id, int $quota): DataResponse {
|
|
$this->checkedGetFolder($id);
|
|
|
|
$this->manager->setFolderQuota($id, $quota);
|
|
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]);
|
|
}
|
|
|
|
/**
|
|
* Toggle the ACL for a Groupfolder
|
|
*
|
|
* @param int $id ID of the Groupfolder
|
|
* @param bool $acl Whether ACL should be enabled or not
|
|
* @return DataResponse<Http::STATUS_OK, array{success: true, folder: GroupFoldersFolder}, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
*
|
|
* 200: ACL toggled successfully
|
|
*/
|
|
#[PasswordConfirmationRequired]
|
|
#[RequireGroupFolderAdmin]
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'POST', url: '/folders/{id}/acl')]
|
|
public function setACL(int $id, bool $acl): DataResponse {
|
|
$this->checkedGetFolder($id);
|
|
|
|
$this->manager->setFolderACL($id, $acl);
|
|
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]);
|
|
}
|
|
|
|
/**
|
|
* Rename a Groupfolder
|
|
*
|
|
* @param int $id ID of the Groupfolder
|
|
* @param string $mountpoint New Mountpoint of the Groupfolder
|
|
* @return DataResponse<Http::STATUS_OK, array{success: true, folder: GroupFoldersFolder}, array{}>
|
|
* @throws OCSNotFoundException Groupfolder not found
|
|
* @throws OCSBadRequestException Mount point already exists or invalid mount point provided
|
|
*
|
|
* 200: Groupfolder renamed successfully
|
|
*/
|
|
#[PasswordConfirmationRequired]
|
|
#[RequireGroupFolderAdmin]
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'POST', url: '/folders/{id}/mountpoint')]
|
|
public function renameFolder(int $id, string $mountpoint): DataResponse {
|
|
$this->checkedGetFolder($id);
|
|
|
|
// Check if the new mountpoint is valid
|
|
if (empty($mountpoint)) {
|
|
throw new OCSBadRequestException('Mount point cannot be empty');
|
|
}
|
|
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
if ($folder->mountPoint === trim($mountpoint)) {
|
|
return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]);
|
|
}
|
|
|
|
$this->checkMountPointExists(trim($mountpoint));
|
|
$this->manager->renameFolder($id, trim($mountpoint));
|
|
|
|
$folder = $this->checkedGetFolder($id);
|
|
|
|
return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]);
|
|
}
|
|
|
|
/**
|
|
* Overwrite response builder to customize xml handling to deal with spaces in folder names
|
|
*/
|
|
private function buildOCSResponseXML(string $format, DataResponse $data): V1Response {
|
|
/** @var array $folderData */
|
|
$folderData = $data->getData();
|
|
if (isset($folderData['id'])) {
|
|
// single folder response
|
|
$folderData = $this->folderDataForXML($folderData);
|
|
} elseif (isset($folderData['folder'])) {
|
|
// single folder response
|
|
$folderData['folder'] = $this->folderDataForXML(['folder']);
|
|
} elseif (is_array($folderData) && count($folderData) && isset(current($folderData)['id'])) {
|
|
// folder list
|
|
$folderData = array_map($this->folderDataForXML(...), $folderData);
|
|
}
|
|
|
|
$data->setData($folderData);
|
|
|
|
return new V1Response($data, $format);
|
|
}
|
|
|
|
private function folderDataForXML(array $data): array {
|
|
$groups = $data['group_details'] ?? [];
|
|
unset($data['group_details']);
|
|
$data['groups'] = [];
|
|
foreach ($groups as $id => $group) {
|
|
$data['groups'][] = [
|
|
'@group_id' => $id,
|
|
'@permissions' => $group['permissions'],
|
|
'@display-name' => $group['displayName'],
|
|
'@type' => $group['type'],
|
|
];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Searches for matching ACL mappings
|
|
*
|
|
* @param int $id The ID of the Groupfolder
|
|
* @param string $search String to search by
|
|
* @return DataResponse<Http::STATUS_OK, array{users: list<GroupFoldersUser>, groups: list<GroupFoldersGroup>, circles: list<GroupFoldersCircle>}, array{}>
|
|
* @throws OCSForbiddenException Not allowed to search
|
|
*
|
|
* 200: ACL Mappings returned
|
|
*/
|
|
#[NoAdminRequired]
|
|
#[FrontpageRoute(verb: 'GET', url: '/folders/{id}/search')]
|
|
public function aclMappingSearch(int $id, string $search = ''): DataResponse {
|
|
$users = $groups = $circles = [];
|
|
|
|
if ($this->user === null) {
|
|
throw new OCSForbiddenException();
|
|
}
|
|
|
|
if ($this->manager->canManageACL($id, $this->user) === true) {
|
|
$groups = $this->manager->searchGroups($id, $search);
|
|
$users = $this->manager->searchUsers($id, $search);
|
|
$circles = $this->manager->searchCircles($id, $search);
|
|
}
|
|
|
|
return new DataResponse([
|
|
'users' => $users,
|
|
'groups' => $groups,
|
|
'circles' => $circles
|
|
]);
|
|
}
|
|
|
|
private function compareFolderNames(string $a, string $b): int {
|
|
if (($value = strnatcmp($a, $b)) === 0) {
|
|
return $value;
|
|
}
|
|
|
|
// Folder names starting with '_' get pushed to the end while they are brought to the
|
|
// beginning in the frontend. Do the same here to keep it consistent with the frontend
|
|
if (strncmp($a, '_', 1) === 0 && strncmp($b, '_', 1) !== 0) {
|
|
return -1;
|
|
}
|
|
if (strncmp($a, '_', 1) !== 0 && strncmp($b, '_', 1) === 0) {
|
|
return 1;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
}
|