rtckit/php-sip

View on GitHub
src/Header/ContactHeader.php

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
<?php
/**
 * RTCKit\SIP\Header\ContactHeader Class
 */
declare(strict_types = 1);

namespace RTCKit\SIP\Header;

use RTCKit\SIP\Response;
use RTCKit\SIP\URI;
use RTCKit\SIP\Exception\InvalidHeaderLineException;
use RTCKit\SIP\Exception\InvalidHeaderParameterException;
use RTCKit\SIP\Exception\InvalidHeaderValueException;

/**
 * Contact header class
 */
class ContactHeader
{
    /** @var list<ContactValue> Contact value(s) */
    public array $values = [];

    /** @var bool Whether value was '*' https://tools.ietf.org/html/rfc3261#section-7.3 */
    public bool $wildcard = false;

    final public function __construct() {}

    /**
     * Contact header value parser
     *
     * @param list<string> $hbody Header body
     * @throws InvalidHeaderLineException
     * @throws InvalidHeaderParameterException
     * @return ContactHeader
     */
    public static function parse(array $hbody): ContactHeader
    {
        $ret = new static;

        $input = $hbody;
        $hbody = [];

        foreach ($input as $hline) {
            $segments = explode(',', $hline);
            $buffer = $comma = '';

            foreach ($segments as $segment) {
                $buffer .= $comma . $segment;

                if (!(substr_count($segment, '"') % 2) && (substr_count($segment, '<') === substr_count($segment, '>'))) {
                    $hbody[] = $buffer;
                    $buffer = $comma = '';

                    continue;
                }

                $comma = ',';
            }

            if (isset($buffer[0])) {
                $hbody[] = $buffer;
            }
        }

        foreach ($hbody as $hline) {
            $val = new ContactValue;

            $len = strlen($hline);
            $fetchParams = false;
            $lastWsp = -1;
            $quoted = false;
            $qfrom = null;
            $afrom = null;
            $base = 0;
            $addr = false;

            for ($i = 0; $i <= $len; $i++) {
                if (!$quoted) {
                    if (($i === $len) || ($hline[$i] === ' ') || ($hline[$i] === "\t")) {
                        if (is_null($afrom)) {
                            $lastWsp = $i;

                            continue;
                        } else {
                            $addr = substr($hline, $afrom, $i - $afrom);
                            $semiPos = strpos($addr, ';');

                            if ($semiPos !== false) {
                                $addr = substr($addr, 0, $semiPos);
                                $i = $semiPos + 1;
                                $fetchParams = true;
                            }

                            if (strpos($addr, '>') !== false) {
                                throw new InvalidHeaderLineException('Invalid contact line, unmatched <> enclosure ending', Response::BAD_REQUEST);
                            }

                            $afrom = null;
                        }
                    } else if ($hline[$i] === '*') {
                        if (($lastWsp === $i - 1) && !isset($ret->values[0]) && !isset($hbody[1]) && !isset($hline[$i + 1])) {
                            $ret->wildcard = true;

                            return $ret;
                        } else {
                            throw new InvalidHeaderLineException('Improper use of * wildcard in Contact header field value', Response::BAD_REQUEST);
                        }
                    } else if ($hline[$i] === ':') {
                        $afrom = $lastWsp + 1;

                        continue;
                    } else if ($hline[$i] === '"') {
                        $quoted = true;
                        $qfrom = $i;

                        continue;
                    } else if ($hline[$i] === '<') {
                        $next = $i + 1;
                        $end = strpos($hline, '>', $next);

                        if ($end === false) {
                            throw new InvalidHeaderLineException('Invalid contact line, unmatched <> enclosure opening', Response::BAD_REQUEST);
                        }

                        $addr = trim(substr($hline, $next, $end - $next));

                        if (strpos($addr, '<') !== false) {
                            throw new InvalidHeaderLineException('Invalid contact line, unmatched <> enclosure opening', Response::BAD_REQUEST);
                        }

                        if (!isset($val->name[0])) {
                            $name = trim(substr($hline, $base, $i - $base));

                            if (isset($name[0])) {
                                $val->name = $name;
                            }
                        }

                        $i = $end + 1;
                        $fetchParams = true;
                    } /* else if ($hline[$i] === '>') {
                        throw new InvalidHeaderLineException('Invalid contact line, unmatched <> enclosure ending', Response::BAD_REQUEST);
                    } */

                    if ($fetchParams) {
                        $commaPos = ($i >= $len) ? false : strpos($hline, ',', $i);
                        $remainder = (($commaPos === false) ? $len : $commaPos) - $i;

                        if ($commaPos !== false) {
                            $base = $commaPos + 1;
                        }

                        if ($remainder > 0) {
                            $params = explode(';', substr($hline, $i, $remainder));

                            foreach ($params as $ord => $param) {
                                $param = trim($param);

                                if (!isset($param[0])) {
                                    if ($ord) {
                                        throw new InvalidHeaderParameterException('Empty header parameters', Response::BAD_REQUEST);
                                    } else {
                                        continue;
                                    }
                                }

                                $p = explode('=', $param);
                                $p[0] = rtrim($p[0]);

                                if (!isset($p[0][0])) {
                                    throw new InvalidHeaderParameterException('Empty header parameters', Response::BAD_REQUEST);
                                }

                                if ($p[0][0] === '>') {
                                    throw new InvalidHeaderLineException('Invalid contact line, unmatched <> enclosure ending', Response::BAD_REQUEST);
                                }

                                $pv = isset($p[1]) ? trim($p[1]) : '';

                                if ($p[0] === 'q') {
                                    if (isset($val->q)) {
                                        throw new InvalidHeaderParameterException('Duplicate q Contact header value parameter', Response::BAD_REQUEST);
                                    }

                                    $val->q = (float) $pv;
                                } else if ($p[0] === 'expires') {
                                    if (isset($val->expires)) {
                                        throw new InvalidHeaderParameterException('Duplicate expires Contact header value parameter', Response::BAD_REQUEST);
                                    }

                                    $val->expires = (int) $pv;
                                } else {
                                    if (isset($val->params[$p[0]])) {
                                        throw new InvalidHeaderParameterException('Duplicate header value parameter: ' . $p[0], Response::BAD_REQUEST);
                                    }

                                    $val->params[$p[0]] = $pv;
                                }
                            }
                        }

                        if (is_string($addr)) {
                            $val->uri = URI::parse($addr);
                            $ret->values[] = $val;
                            $addr = null;
                            $val = new ContactValue;
                        }

                        if (($commaPos === false) || ($remainder <= 0)) {
                            break;
                        } else {
                            $fetchParams = false;
                            $i = $commaPos + 1;
                        }
                    }
                } else if ($i === $len) {
                    throw new InvalidHeaderLineException('Invalid contact line, unmatched "" quote enclosure ending', Response::BAD_REQUEST);
                } else if($hline[$i] === '\\') {
                    $i++;
                } else if($hline[$i] === '"') {
                    $quoted = false;
                    /** @psalm-suppress PossiblyNullOperand qfrom is always set if fetchParams === true */
                    $val->name = str_replace('\\\\', '\\', substr($hline, $qfrom + 1, $i - $qfrom - 1));
                    $qfrom = null;
                }
            }

            if (is_string($addr)) {
                $val->uri = URI::parse($addr);
                $ret->values[] = $val;
            }
        }

        return $ret;
    }

    /**
     * Contact header values renderer
     *
     * @param string $hname Header field name
     * @throws InvalidHeaderValueException
     * @return string
     */
    public function render(string $hname): string
    {
        if ($this->wildcard) {
            return "{$hname}: *\r\n";
        }

        if (!isset($this->values[0])) {
            throw new InvalidHeaderValueException('Missing Contact header values');
        }

        $ret = "{$hname}: ";
        $delim = '';

        foreach ($this->values as $value) {
            if (!isset($value->uri)) {
                throw new InvalidHeaderValueException('Missing address part for contact header field value');
            }

            $addr = $value->uri->render();

            if (isset($value->name[0])) {
                $ret .= $delim . '"' . addcslashes($value->name, "\x5c") . '" ' . "<{$addr}>";
            } else {
                $ret .= "{$delim}<{$addr}>";
            }

            if (isset($value->q)) {
                $ret .= ";q={$value->q}";
            }

            if (isset($value->expires)) {
                $ret .= ";expires={$value->expires}";
            }

            foreach ($value->params as $pk => $pv) {
                $ret .= ";{$pk}" . (!isset($pv[0]) ? '' : "={$pv}");
            }

            $delim = ', ';
        }

        return $ret . "\r\n";
    }
}