1<?php 2 3/** 4 * This file is part of the FreeDSx LDAP package. 5 * 6 * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace FreeDSx\Ldap\Search\Filter; 13 14use FreeDSx\Asn1\Asn1; 15use FreeDSx\Asn1\Exception\EncoderException; 16use FreeDSx\Asn1\Type\AbstractType; 17use FreeDSx\Asn1\Type\IncompleteType; 18use FreeDSx\Asn1\Type\OctetStringType; 19use FreeDSx\Asn1\Type\SequenceType; 20use FreeDSx\Ldap\Entry\Attribute; 21use FreeDSx\Ldap\Exception\ProtocolException; 22use FreeDSx\Ldap\Exception\RuntimeException; 23use FreeDSx\Ldap\Protocol\LdapEncoder; 24 25/** 26 * Represents a substring filter. RFC 4511, 4.5.1.7.2. 27 * 28 * SubstringFilter ::= SEQUENCE { 29 * type AttributeDescription, 30 * substrings SEQUENCE SIZE (1..MAX) OF substring CHOICE { 31 * initial [0] AssertionValue, -- can occur at most once 32 * any [1] AssertionValue, 33 * final [2] AssertionValue } -- can occur at most once 34 * } 35 * 36 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 37 */ 38class SubstringFilter implements FilterInterface 39{ 40 use FilterAttributeTrait; 41 42 protected const CHOICE_TAG = 4; 43 44 /** 45 * @var null|string 46 */ 47 protected $startsWith; 48 49 /** 50 * @var null|string 51 */ 52 protected $endsWith; 53 54 /** 55 * @var string[] 56 */ 57 protected $contains = []; 58 59 /** 60 * @param string $attribute 61 * @param null|string $startsWith 62 * @param null|string $endsWith 63 * @param string ...$contains 64 */ 65 public function __construct(string $attribute, ?string $startsWith = null, ?string $endsWith = null, string ...$contains) 66 { 67 $this->attribute = $attribute; 68 $this->startsWith = $startsWith; 69 $this->endsWith = $endsWith; 70 $this->contains = $contains; 71 } 72 73 /** 74 * Get the value that it should start with. 75 * 76 * @return null|string 77 */ 78 public function getStartsWith(): ?string 79 { 80 return $this->startsWith; 81 } 82 83 /** 84 * Set the value it should start with. 85 * 86 * @param null|string $value 87 * @return $this 88 */ 89 public function setStartsWith(?string $value) 90 { 91 $this->startsWith = $value; 92 93 return $this; 94 } 95 96 /** 97 * Get the value it should end with. 98 * 99 * @return null|string 100 */ 101 public function getEndsWith(): ?string 102 { 103 return $this->endsWith; 104 } 105 106 /** 107 * Set the value it should end with. 108 * 109 * @param null|string $value 110 * @return $this 111 */ 112 public function setEndsWith(?string $value) 113 { 114 $this->endsWith = $value; 115 116 return $this; 117 } 118 119 /** 120 * Get the values it should contain. 121 * 122 * @return string[] 123 */ 124 public function getContains(): array 125 { 126 return $this->contains; 127 } 128 129 /** 130 * Set the values it should contain. 131 * 132 * @param string ...$values 133 * @return $this 134 */ 135 public function setContains(string ...$values) 136 { 137 $this->contains = $values; 138 139 return $this; 140 } 141 142 /** 143 * @throws RuntimeException 144 */ 145 public function toAsn1(): AbstractType 146 { 147 if ($this->startsWith === null && $this->endsWith === null && count($this->contains) === 0) { 148 throw new RuntimeException('You must provide a contains, starts with, or ends with value to the substring filter.'); 149 } 150 $substrings = Asn1::sequenceOf(); 151 152 if ($this->startsWith !== null) { 153 $substrings->addChild(Asn1::context(0, Asn1::octetString($this->startsWith))); 154 } 155 156 foreach ($this->contains as $contain) { 157 $substrings->addChild(Asn1::context(1, Asn1::octetString($contain))); 158 } 159 160 if ($this->endsWith !== null) { 161 $substrings->addChild(Asn1::context(2, Asn1::octetString($this->endsWith))); 162 } 163 164 return Asn1::context(self::CHOICE_TAG, Asn1::sequence( 165 Asn1::octetString($this->attribute), 166 $substrings 167 )); 168 } 169 170 /** 171 * {@inheritdoc} 172 */ 173 public function toString(): string 174 { 175 $filter = self::PAREN_LEFT . $this->attribute . self::FILTER_EQUAL; 176 177 $value = ''; 178 if (count($this->contains) !== 0) { 179 $value = array_map(function ($value) { 180 return Attribute::escape($value); 181 }, $this->contains); 182 $value = '*' . implode('*', $value) . '*'; 183 } 184 if ($this->startsWith !== null) { 185 $startsWith = Attribute::escape($this->startsWith); 186 $value = ($value === '' ? $startsWith . '*' : $startsWith) . $value; 187 } 188 if ($this->endsWith !== null) { 189 $endsWith = Attribute::escape($this->endsWith); 190 $value = $value . ($value === '' ? '*' . $endsWith : $endsWith); 191 } 192 193 return $filter . $value . self::PAREN_RIGHT; 194 } 195 196 /** 197 * {@inheritDoc} 198 * @return SubstringFilter 199 * @throws EncoderException 200 */ 201 public static function fromAsn1(AbstractType $type) 202 { 203 $encoder = new LdapEncoder(); 204 $type = $type instanceof IncompleteType ? $encoder->complete($type, AbstractType::TAG_TYPE_SEQUENCE) : $type; 205 if (!($type instanceof SequenceType && count($type->getChildren()) === 2)) { 206 throw new ProtocolException('The substring type is malformed'); 207 } 208 209 $attrType = $type->getChild(0); 210 $substrings = $type->getChild(1); 211 if (!($attrType instanceof OctetStringType && $substrings instanceof SequenceType && count($substrings) > 0)) { 212 throw new ProtocolException('The substring filter is malformed.'); 213 } 214 [$startsWith, $endsWith, $contains] = self::parseSubstrings($substrings); 215 216 return new self($attrType->getValue(), $startsWith, $endsWith, ...$contains); 217 } 218 219 /** 220 * @psalm-return array{0: mixed|null, 1: mixed|null, 2: list<mixed>} 221 * @throws ProtocolException 222 */ 223 protected static function parseSubstrings(SequenceType $substrings): array 224 { 225 $startsWith = null; 226 $endsWith = null; 227 $contains = []; 228 229 /** @var AbstractType $substring */ 230 foreach ($substrings->getChildren() as $substring) { 231 if ($substring->getTagClass() !== AbstractType::TAG_CLASS_CONTEXT_SPECIFIC) { 232 throw new ProtocolException('The substring filter is malformed.'); 233 } 234 # Starts With and Ends With can occur only once each. Contains can occur multiple times. 235 if ($substring->getTagNumber() === 0) { 236 if ($startsWith !== null) { 237 throw new ProtocolException('The substring filter is malformed.'); 238 } else { 239 $startsWith = $substring; 240 } 241 } elseif ($substring->getTagNumber() === 1) { 242 $contains[] = $substring->getValue(); 243 } elseif ($substring->getTagNumber() === 2) { 244 if ($endsWith !== null) { 245 throw new ProtocolException('The substring filter is malformed.'); 246 } else { 247 $endsWith = $substring; 248 } 249 } else { 250 throw new ProtocolException('The substring filter is malformed.'); 251 } 252 } 253 254 return [ 255 ($startsWith !== null) ? $startsWith->getValue() : null, 256 ($endsWith !== null) ? $endsWith->getValue() : null, 257 $contains 258 ]; 259 } 260} 261