Skip to content

Files Folder Control Developer Guide

Ed Mozley edited this page Nov 15, 2026 · 1 revision

πŸ”’ Files: deleting or moving a folder - Developer Guide

5.4.0 Β· Issue #293 Β· User page: Files Β§ Deleting or moving a folder Β· Decisions: Recycle bin - design

Deleting or moving a folder takes everything in it along. So both need Modify on the folder and on every folder inside it, however deep - as Windows refuses to delete or move a folder holding something you have no right to change. This page is how that is checked, how the refusal is worded so it never leaks a name, and the administrator's tool for seeing the whole picture.


1. πŸ“ The files

🧠 rule Β· πŸ”Œ enforced Β· πŸ› οΈ admin tool Β· πŸ–₯️ help Β· πŸ§ͺ test

🎨 File What it does
🧠 includes/files/acl.php FilesAcl::subtreeTopFirst(), blockersInside(), refusalInside() - the rule, once
πŸ”Œ api/files/folder.php delete: filesNeed(MODIFY) on the folder, then refusalInside()
πŸ”Œ api/files/transfer.php filesTransferFolder() move: the same check, verb 'move'
πŸ› οΈ api/system/debug-tools/D020_files_folder_delete.php D020 - the report, administrators only
πŸ› οΈ system/debug-tools/d020/index.php + system/debug-tools/includes/tools.php The tool page and its registry entry; debugToolFilesFolderOptions() lists every folder by path
πŸ–₯️ system/help/debug-tools.php, lang/en/files.php (help.permissions.p7) Help
πŸ§ͺ tests/files-folder-delete-refusal.php 24 checks over real HTTP, including a 10 Γ— 10 Γ— 10 tree with one locked folder at the bottom

Not touched, on purpose: copying. A folder copy (filesCopyFolderTree()) already brings only what the copier may Download - it takes nothing away from anyone, so it needs no such check. Files have no permissions of their own (a file is controlled by its folder), so checking every folder covers every file.


2. The check

subtreeTopFirst() walks the live tree breadth-first, children by name, so "the first blocker" is always the same one and the nearest the top. blockersInside() keeps the folders inside (depth > 0) below Modify:

public static function blockersInside(PDO $conn, int $analystId, int $folderId): array
{
    $levels = self::levels($conn, $analystId);
    $out = [];
    foreach (self::subtreeTopFirst($conn, $folderId) as $s) {
        if ($s['depth'] > 0 && ($levels[$s['id']] ?? 0) < self::MODIFY) $out[] = $s['id'];
    }
    return $out;
}

There is no depth limit. One levels() call resolves every folder for the person (cached), so a 1,112-folder tree is one pass.

The endpoints check Modify on the folder itself first (filesNeed() / filesTransferFolder()), then:

$refusal = FilesAcl::refusalInside($conn, $analystId, $id);          // 'move' in transfer.php
if ($refusal !== null) {
    filesAudit($conn, $analystId, 'denied', ['folder_id' => $id],
               ['wanted' => 'modify', 'doing' => 'delete', 'blocked_inside' => count(FilesAcl::blockersInside(...))]);
    filesApiFail($refusal);
}

3. The message never says more than the person can see

πŸ”΄ TRAP: in refusalInside(). A refusal that listed what is in the way would be a way to read the names of folders kept from you: try to delete the parent, read the error. So:

  • It names one blocking folder, and only one the person can View: You cannot delete "Projects": you do not have permission to change "HR", and other items inside it. Ask whoever manages it.
  • When every blocker is hidden from them, it names none and gives no count: You cannot delete "Projects": it contains folders you do not have access to. Ask whoever manages this folder.
  • Never a list: 300 locked subfolders are one sentence.

That a refusal happens at all says something is inside. That cannot be avoided - Windows says the same - but the name stays hidden.


4. D020 - the administrator's view

System β†’ Debug Tools β†’ D020 - Files: can this person delete or move this folder? Pick a person and a folder. Plain-text report:

  • The verdict - PASS, or FAIL with the exact sentence the person is shown (from the same refusalInside(), so it cannot drift).
  • Everything inside as a tree: each folder with the person's level, whether it inherits, PASS or FAIL; files under their folder, passing or failing with it (first 25 per folder).
  • Why for each FAIL: inheritance off with nothing granted to them or their teams, a grant below Modify, or nothing granted anywhere above.

Administrators only, fail closed (403 otherwise) - it prints names the person may not see. Read-only.


5. Tests

tests/files-folder-delete-refusal.php, against a throwaway install, as an administrator (Files permissions give an administrator nothing extra):

  • the rule directly: no blockers when everything inherits Modify; the visible one named; the hidden one never; no name and no count when all are hidden;
  • over HTTP through api/files/folder.php and api/files/transfer.php: refused, with the right message, audited; a normal folder still deletes;
  • buried deep: 10 folders Γ— 10 Γ— 10 with one locked folder (and a file) at the bottom - delete and move refused, naming nothing; D020 shows exactly two FAIL lines, that folder and its file;
  • controls: grant Modify on the blockers and the same delete and move go through.

See also: Files - Developer Guide Β· Recycle bin - Developer Guide Β· Code traps

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Responses to feedback

Blue sky thinking

Bugs resolved

Pull requests

Links

Clone this wiki locally