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