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
8
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%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 classifyBodyResponse
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
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\IcapClientException;
25use Ndrstmr\Icap\Exception\IcapProtocolException;
26use Ndrstmr\Icap\Exception\IcapServerException;
27
28/**
29 * Default strategy for the answer to a preview (RFC 3507 §4.5).
30 *
31 * A server answers a preview with 100 Continue (send the rest of the body),
32 * 204 No Content (no change) or a final response, which may come before the
33 * rest of the body was sent. This class applies the standard rules, which
34 * are verified against no ICAP product: 204 counts as clean, and a 200/206
35 * is infected when it carries one of the configured virus headers. The
36 * verified mode ({@see CicapVerdict}) does not ask this class.
37 */
38final class DefaultPreviewStrategy implements PreviewStrategyInterface
39{
40    /** @var list<string> */
41    private array $virusFoundHeaders;
42
43    /**
44     * @param list<string> $virusFoundHeaders Ordered list of vendor virus-name
45     *   headers to inspect when a 200/206 preview response arrives.  The first
46     *   header present in the response wins.  Defaults to ['X-Virus-Name'] for
47     *   backward compatibility; production deployments should pass
48     *   Config::getVirusFoundHeaders().
49     */
50    public function __construct(array $virusFoundHeaders = ['X-Virus-Name'])
51    {
52        $this->virusFoundHeaders = $virusFoundHeaders;
53    }
54
55    /**
56     * Decide whether to continue sending the body after a preview response.
57     *
58     * Status semantics:
59     * - 100 Continue   → server wants the rest of the body.
60     * - 204 No Content → server changes nothing; clean under the standard rules.
61     * - 200 / 206      → final response; inspect virus headers to
62     *                    determine whether the file is infected or clean.
63     * - 4xx            → {@see IcapClientException}, 5xx → {@see IcapServerException}:
64     *                    the same types as for the answer to any other request.
65     * - anything else  → protocol error.
66     *
67     * @throws IcapClientException   When the status is in the 4xx range
68     * @throws IcapServerException   When the status is in the 5xx range
69     * @throws IcapProtocolException When the status is none of the above
70     */
71    #[\Override]
72    public function handlePreviewResponse(IcapResponse $previewResponse): PreviewDecision
73    {
74        $code = $previewResponse->statusCode;
75
76        return match (true) {
77            $code === 100 => PreviewDecision::CONTINUE_SENDING,
78            $code === 204 => PreviewDecision::ABORT_CLEAN,
79            $code === 200,
80            $code === 206 => $this->classifyBodyResponse($previewResponse),
81            default => throw IcapStatus::failure($code) ?? new IcapProtocolException(
82                'Unexpected preview status code: ' . $code,
83                $code,
84            ),
85        };
86    }
87
88    /**
89     * Inspect the 200/206 response for vendor virus headers.
90     *
91     * Returns ABORT_INFECTED if any configured virus header is present, in
92     * any spelling, ABORT_CLEAN otherwise. Uses the same lookup as the scan
93     * verdict in {@see IcapClient}, so both paths agree.
94     */
95    private function classifyBodyResponse(IcapResponse $response): PreviewDecision
96    {
97        return HeaderFields::first($response->headers, $this->virusFoundHeaders) !== null
98            ? PreviewDecision::ABORT_INFECTED
99            : PreviewDecision::ABORT_CLEAN;
100    }
101}