xref: /dokuwiki/_test/tests/inc/media_upload.test.php (revision 0579c2f8a6cf8c76cb756b91a4ab1167dcffd4af)
1<?php
2
3/**
4 * Tests for the ACL enforcement around media uploads.
5 *
6 * media_save() trusts the authorization level supplied by its caller so that
7 * delegated saves (for example the bureaucracy plugin's "runas" uploads) keep
8 * working. The public upload endpoints media_upload() and media_upload_xhr()
9 * must therefore authorize the resolved target id themselves, because the
10 * requested media id may contain namespace separators pointing into a
11 * namespace other than the one the upload form was opened for.
12 */
13class media_upload_test extends DokuWikiTest {
14
15    public function setUp(): void {
16        parent::setUp();
17
18        global $conf, $AUTH_ACL, $USERINFO, $MSG;
19
20        $conf['useacl'] = 1;
21        $_SERVER['REMOTE_USER'] = 'john';
22        $USERINFO['grps'] = ['user'];
23        $MSG = [];
24
25        // upload allowed in public:* and public:sub:*, but denied in the
26        // sibling child namespace public:private:*
27        $AUTH_ACL = [
28            '*                  @ALL           0',
29            'public:*           @user          8', // AUTH_UPLOAD
30            'public:sub:*       @user          8', // AUTH_UPLOAD
31            'public:private:*   @user          0', // AUTH_NONE
32        ];
33    }
34
35    /**
36     * Provide a fresh temporary copy of a valid PNG.
37     *
38     * @return string path to the temporary file
39     */
40    protected function tmpImage() {
41        global $conf;
42        $orig = mediaFN('wiki:dokuwiki-128.png');
43        $tmp = $conf['tmpdir'] . '/media_upload_test_' . md5($orig . microtime()) . '.png';
44        copy($orig, $tmp);
45        return $tmp;
46    }
47
48    /**
49     * Simulate a media_upload() request for the given namespace and media id.
50     *
51     * @param string $ns      the namespace the upload form was opened for
52     * @param string $mediaid the requested (possibly nested) media id
53     * @return false|string the media_upload() return value
54     */
55    protected function upload($ns, $mediaid) {
56        global $MSG;
57        $MSG = [];
58
59        $_POST['mediaid'] = $mediaid;
60        $_POST['ow'] = '';
61        $_REQUEST['sectok'] = getSecurityToken();
62
63        $file = [
64            'name' => noNS($mediaid),
65            'tmp_name' => $this->tmpImage(),
66            'error' => 0,
67            'size' => 100,
68        ];
69
70        return media_upload($ns, AUTH_UPLOAD, $file);
71    }
72
73    /**
74     * Collect the messages emitted during the last request.
75     *
76     * @return string[]
77     */
78    protected function messages() {
79        global $MSG;
80        return array_map(static fn($m) => $m['msg'], $MSG ?? []);
81    }
82
83    /**
84     * A nested media id must be authorized against its own namespace, so an
85     * upload into an ACL-denied child namespace is rejected even though the
86     * form was opened for a permitted parent namespace.
87     */
88    public function test_upload_rejects_denied_child_namespace() {
89        $res = $this->upload('public', 'private:target.png');
90
91        $this->assertFalse($res);
92        $this->assertContains(
93            "You don't have permissions to upload files.",
94            $this->messages(),
95            'the upload must be denied by the ACL check'
96        );
97        $this->assertFileDoesNotExist(mediaFN('public:private:target.png'));
98    }
99
100    /**
101     * A nested media id into a permitted namespace must pass the ACL check.
102     *
103     * The actual move fails in the test environment (is_uploaded_file()), so we
104     * only assert that the request got past authorization.
105     */
106    public function test_upload_allows_permitted_child_namespace() {
107        $res = $this->upload('public', 'sub:target.png');
108
109        $this->assertFalse($res);
110        $this->assertNotContains(
111            "You don't have permissions to upload files.",
112            $this->messages(),
113            'the upload must pass the ACL check for a permitted target'
114        );
115    }
116
117    /**
118     * media_save() must trust the authorization level supplied by the caller,
119     * so a privileged (delegated) save succeeds even when the current session
120     * user has no access to the target namespace.
121     */
122    public function test_save_trusts_supplied_auth() {
123        $id = 'public:private:delegated.png';
124        $res = media_save(
125            ['name' => $this->tmpImage(), 'mime' => 'image/png', 'ext' => 'png'],
126            $id,
127            false,
128            AUTH_UPLOAD,
129            'copy'
130        );
131
132        $this->assertSame($id, $res);
133        $this->assertFileExists(mediaFN($id));
134    }
135
136    /**
137     * media_save() must still reject a caller that supplies an insufficient
138     * authorization level.
139     */
140    public function test_save_enforces_supplied_auth() {
141        $id = 'public:target.png';
142        $res = media_save(
143            ['name' => $this->tmpImage(), 'mime' => 'image/png', 'ext' => 'png'],
144            $id,
145            false,
146            AUTH_READ,
147            'copy'
148        );
149
150        $this->assertIsArray($res);
151        $this->assertSame(-1, $res[1]);
152        $this->assertFileDoesNotExist(mediaFN($id));
153    }
154}
155