Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
DefaultPreviewStrategy
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
3 / 3
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handlePreviewResponse
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 classifyBodyResponse
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3/**
4 * SPDX-License-Identifier: EUPL-1.2
5 *
6 * This file is part of icap-flow.
7 *
8 * Licensed under the EUPL, Version 1.2 only (the "Licence");
9 * you may not use this work except in compliance with the Licence.
10 * You may obtain a copy of the Licence at:
11 *
12 *     https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the Licence is distributed on an "AS IS" basis,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 */
18
19declare(strict_types=1);
20
21namespace Ndrstmr\Icap;
22
23use Ndrstmr\Icap\DTO\IcapResponse;
24use Ndrstmr\Icap\Exception\IcapProtocolException;
25
26/**
27 * Default strategy for interpreting preview responses.
28 *
29 * Handles RFC 3507 §4.3.3 / §6: servers may respond 200 or 206 during a
30 * preview exchange when malware is detected in the first chunk — without
31 * waiting for the full body.  Earlier versions of this class threw on
32 * 200/206, making the ABORT_INFECTED branch in {@see IcapClient}
33 * unreachable.  Fixed in v2.1.1 (finding B, 3/4 reviewers).
34 */
35final class DefaultPreviewStrategy implements PreviewStrategyInterface
36{
37    /** @var list<string> */
38    private array $virusFoundHeaders;
39
40    /**
41     * @param list<string> $virusFoundHeaders Ordered list of vendor virus-name
42     *   headers to inspect when a 200/206 preview response arrives.  The first
43     *   header present in the response wins.  Defaults to ['X-Virus-Name'] for
44     *   backward compatibility; production deployments should pass
45     *   Config::getVirusFoundHeaders().
46     */
47    public function __construct(array $virusFoundHeaders = ['X-Virus-Name'])
48    {
49        $this->virusFoundHeaders = $virusFoundHeaders;
50    }
51
52    /**
53     * Decide whether to continue sending the body after a preview response.
54     *
55     * Status semantics:
56     * - 100 Continue   → server wants the rest of the body.
57     * - 204 No Content → server is done; file is clean.
58     * - 200 / 206      → server finished early; inspect virus headers to
59     *                    determine whether the file is infected or clean.
60     * - anything else  → protocol error.
61     */
62    #[\Override]
63    public function handlePreviewResponse(IcapResponse $previewResponse): PreviewDecision
64    {
65        return match (true) {
66            $previewResponse->statusCode === 100 => PreviewDecision::CONTINUE_SENDING,
67            $previewResponse->statusCode === 204 => PreviewDecision::ABORT_CLEAN,
68            $previewResponse->statusCode === 200,
69            $previewResponse->statusCode === 206 => $this->classifyBodyResponse($previewResponse),
70            default => throw new IcapProtocolException(
71                'Unexpected preview status code: ' . $previewResponse->statusCode,
72                $previewResponse->statusCode,
73            ),
74        };
75    }
76
77    /**
78     * Inspect the 200/206 response for vendor virus headers.
79     *
80     * Returns ABORT_INFECTED if any configured virus header is present with a
81     * non-empty value, ABORT_CLEAN otherwise.
82     */
83    private function classifyBodyResponse(IcapResponse $response): PreviewDecision
84    {
85        foreach ($this->virusFoundHeaders as $header) {
86            if (isset($response->headers[$header]) && $response->headers[$header] !== []) {
87                return PreviewDecision::ABORT_INFECTED;
88            }
89        }
90
91        return PreviewDecision::ABORT_CLEAN;
92    }
93}