Lines 80.00% 40 / 50
Methods 60.00% 3 / 5
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 send 60.00% 3 / 5 0.00% 0 / 1 3.58
 getResponse 100.00% 5 / 5 100.00% 1 / 1 1
 setHttpHeaders 78.94% 30 / 38 0.00% 0 / 1 5.23
 streamContent 100.00% 1 / 1 100.00% 1 / 1 1
41class FileDownloader
42{
43    /** HTTP Content Disposition. */
44    private string $disposition = HeaderUtils::DISPOSITION_INLINE;
45
46    /** HTTP streaming data length. */
47    private readonly int $size;
48
49    private Response $response;
50
51    /**
52     * Constructor.
53     *
54     * @param string $type Type
55     * @param string $content Content
56     */
57    public function __construct(
58        private readonly string $type,
59        private readonly string $content,
60    ) {
61        $this->size = strlen($this->content);
62    }
63
64    /**
65     * Sends data.
66     *
67     * @param string $disposition Disposition
68     * @throws Exception
69     */
70    public function send(string $disposition): void
71    {
72        // Sanity checks
73        if (headers_sent()) {
74            throw new Exception(message: 'Error: unable to send my headers: someone already sent other headers!');
75        }
76
77        if (ob_get_contents()) {
78            throw new Exception(message: 'Error: unable to send my data: someone already sent other data!');
79        }
80
81        $this->getResponse($disposition)->send();
82    }
83
84    /**
85     * Builds the streaming response so it can be returned by a controller and
86     * sent by the HTTP kernel, instead of being flushed directly to the client.
87     *
88     * @param string $disposition Disposition
89     */
90    public function getResponse(string $disposition): Response
91    {
92        $this->disposition = $disposition;
93
94        $this->response = new Response();
95        $this->setHttpHeaders();
96        $this->response->setContent(content: $this->streamContent());
97
98        return $this->response;
99    }
100
101    /**
102     * Sends HTTP Headers.
103     */
104    private function setHttpHeaders(): void
105    {
106        // Evaluate data upon export type request
107        switch ($this->type) {
108            case 'pdf':
109                $filename = 'phpmyfaq.pdf';
110                $description = 'phpMyFAQ PDF export file';
111                $mimeType = 'application/pdf';
112                break;
113            case 'csv':
114                $filename = 'phpmyfaq.csv';
115                $description = 'phpMyFAQ CSV export file';
116                $mimeType = 'text/csv';
117                break;
118            case 'json':
119                $filename = 'phpmyfaq.json';
120                $description = 'phpMyFAQ JSON export file';
121                $mimeType = 'application/json';
122                break;
123            // In this case, no default statement is required:
124            // the one above is just for clean coding style
125            default:
126                $filename = 'phpmyfaq.pmf';
127                $description = 'phpMyFAQ Generic export file';
128                $mimeType = 'application/octet-stream';
129                break;
130        }
131
132        $filename = Export::getExportTimestamp() . '_' . $filename;
133
134        // Set the correct HTTP headers:
135        // 1. Prevent proxies&browsers caching
136        $this->response->setLastModified(date: new DateTime());
137        $this->response->setExpires(date: new DateTime());
138        $this->response->setCache(options: [
139            'must_revalidate' => true,
140            'no_cache' => true,
141            'no_store' => true,
142            'no_transform' => false,
143            'public' => false,
144            'private' => true,
145        ]);
146
147        // 2. Set the correct values for file streaming
148        $this->response->headers->set(key: 'Content-Type', values: $mimeType);
149
150        // 3. RFC2616, ยง19.5.1: $filename must be a quoted-string
151        $disposition = HeaderUtils::makeDisposition(disposition: $this->disposition, filename: $filename);
152        $this->response->headers->set(key: 'Content-Disposition', values: $disposition);
153        $this->response->headers->set(key: 'Content-Description', values: $description);
154        $this->response->headers->set(key: 'Content-Transfer-Encoding', values: 'binary');
155        $this->response->headers->set(key: 'Accept-Ranges', values: 'none');
156        $this->response->headers->set(key: 'Content-Length', values: (string) $this->size);
157    }
158
159    /**
160     * Streams the content.
161     */
162    private function streamContent(): string
163    {
164        return $this->content;
165    }
166}