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