PluginProbe ʕ •ᴥ•ʔ
Search Regex / trunk
Search Regex vtrunk
trunk 1.4.12 1.4.13 1.4.14 1.4.15 1.4.16 2.0 2.0.1 2.1 2.2 2.2.1 2.3 2.3.1 2.3.2 2.3.3 2.4 2.4.1 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1 3.1.1 3.1.2 3.2 3.3 3.3.0 3.3.1 3.4 3.4.1 3.4.2
search-regex / includes / modifier / value / class-string-value.php
search-regex / includes / modifier / value Last commit date
class-date-value.php 5 months ago class-integer-value.php 5 months ago class-key-value.php 5 months ago class-member-value.php 5 months ago class-string-value.php 5 months ago
class-string-value.php
266 lines
1 <?php
2
3 namespace SearchRegex\Modifier\Value;
4
5 use SearchRegex\Modifier;
6 use SearchRegex\Schema;
7 use SearchRegex\Source;
8 use SearchRegex\Search;
9 use SearchRegex\Context;
10 use SearchRegex\Filter;
11 use SearchRegex\Action;
12
13 /**
14 * Modify a string
15 *
16 * @phpstan-type StringModifierOption array{
17 * column?: string,
18 * operation?: 'set'|'replace',
19 * searchValue?: string,
20 * replaceValue?: string,
21 * posId?: int|string,
22 * searchFlags?: array<'case'|'regex'>|'case'|'regex'
23 * }
24 */
25 class String_Value extends Modifier\Modifier {
26 const BEFORE = '<SEARCHREGEX>';
27 const AFTER = '</SEARCHREGEX>';
28
29 /**
30 * Value to search for. Only used in a search/replace
31 */
32 private ?string $search_value = null;
33
34 /**
35 * Value to replace, or the column value in a 'set'
36 */
37 private ?string $replace_value = null;
38
39 /**
40 * Search flags
41 */
42 private Search\Flags $search_flags;
43
44 /**
45 * Position within the column to replace
46 */
47 private ?int $pos_id = null;
48
49 /**
50 * Constructor
51 *
52 * @param StringModifierOption $option String modification options.
53 * @param Schema\Column $schema Schema.
54 */
55 public function __construct( $option, Schema\Column $schema ) {
56 parent::__construct( $option, $schema );
57
58 // @phpstan-ignore booleanAnd.rightAlwaysTrue
59 if ( isset( $option['searchValue'] ) && is_string( $option['searchValue'] ) ) {
60 $this->search_value = $option['searchValue'];
61 }
62
63 // @phpstan-ignore booleanAnd.rightAlwaysTrue
64 if ( isset( $option['replaceValue'] ) && is_string( $option['replaceValue'] ) ) {
65 $this->replace_value = $option['replaceValue'];
66 }
67
68 $this->operation = 'set';
69 if ( isset( $option['operation'] ) && in_array( $option['operation'], [ 'set', 'replace' ], true ) ) {
70 $this->operation = $option['operation'];
71 }
72
73 if ( isset( $option['posId'] ) ) {
74 $this->pos_id = intval( $option['posId'], 10 );
75 }
76
77 $flags = $option['searchFlags'] ?? [ 'case' ];
78 if ( ! is_array( $flags ) ) {
79 $flags = [ $flags ];
80 }
81
82 $this->search_flags = new Search\Flags( $flags );
83 }
84
85 public function is_valid() {
86 if ( $this->operation === 'replace' && $this->search_value === '' ) {
87 return false;
88 }
89
90 return parent::is_valid();
91 }
92
93 public function to_json() {
94 $parent_json = parent::to_json();
95
96 return [
97 'column' => $parent_json['column'],
98 'source' => $parent_json['source'],
99 'operation' => $this->operation,
100 'searchValue' => $this->search_value,
101 'replaceValue' => $this->replace_value,
102 'searchFlags' => $this->search_flags->to_json(),
103 ];
104 }
105
106 /**
107 * Return all the replace positions - the positions within the content where the search is matched.
108 *
109 * @param string $value Value to search and replace within.
110 * @return array<int, string> Array of match positions
111 */
112 public function get_replace_positions( $value ) {
113 if ( ! $this->search_value || $this->replace_value === null ) {
114 return [];
115 }
116
117 $replace_value = $this->replace_value;
118 if ( ! $this->search_flags->is_regex() ) {
119 // Escape the replace value, in case it has a $ in it
120 $replace_value = \preg_replace( '/(?<!\\\)\$/', '\\$', $this->replace_value );
121 }
122
123 // Global replace
124 $result = $this->replace_all( $this->search_value, self::BEFORE . $replace_value . self::AFTER, $value );
125
126 // Split into array
127 $pattern = '@' . self::BEFORE . '(.*?)' . self::AFTER . '@s';
128 if ( $this->search_flags->is_case_insensitive() ) {
129 $pattern .= 'i';
130 }
131
132 if ( \preg_match_all( $pattern, $result, $searches ) > 0 ) {
133 return $searches[1];
134 }
135
136 return [];
137 }
138
139 /**
140 * Perform a global replacement
141 *
142 * @internal
143 * @param string $search Search string.
144 * @param string $replace Replacement value.
145 * @param string $value Content to replace.
146 * @return string
147 */
148 private function replace_all( $search, $replace, $value ) {
149 $pattern = Search\Text::get_pattern( $search, $this->search_flags );
150
151 if ( ! $this->search_flags->is_regex() && is_serialized( $value ) ) {
152 $serial = '/s:(\d*):"(.*?)";/s';
153
154 return (string) preg_replace_callback(
155 $serial, function ( $matches ) use ( $search, $replace ) {
156 if ( strpos( $matches[2], $search ) !== false ) {
157 $replaced = str_replace( $search, $replace, $matches[2] );
158
159 return 's:' . (string) strlen( $replaced ) . ':"' . $replaced . '";';
160 }
161
162 return $matches[0];
163 }, $value
164 );
165 }
166
167 // Global replace
168 return (string) preg_replace( $pattern, $replace, $value );
169 }
170
171 public function perform( $row_id, $row_value, Source\Source $source, Search\Column $column, array $raw, $save_mode ) {
172 if ( $this->operation === 'set' ) {
173 // Identical - just return value
174 if ( $this->replace_value === $row_value ) {
175 return $column;
176 }
177
178 $value = apply_filters( 'searchregex_text', $this->replace_value, $row_id, $row_value, $raw, $source->get_schema_item() );
179
180 if ( $value !== $row_value ) {
181 $replacement = new Context\Type\Replace( $row_value );
182 $replacement->set_replacement( $value );
183 $column->set_contexts( [ $replacement ] );
184 }
185
186 return $column;
187 }
188
189 if ( ! $this->search_value ) {
190 return $column;
191 }
192
193 if ( $this->pos_id === null ) {
194 if ( $this->replace_value === null ) {
195 return $column;
196 }
197
198 // When not saving we need to return the individual replacements. If saving then we want to return the whole text
199 if ( $save_mode ) {
200 $global_replace = $this->replace_all( $this->search_value, $this->replace_value, $row_value );
201
202 $value = apply_filters( 'searchregex_text', $global_replace, $row_id, $row_value, $raw, $source->get_schema_item() );
203
204 // Global replace
205 if ( $row_value !== $value ) {
206 $replacement = new Context\Type\Replace( $row_value );
207 $replacement->set_replacement( $value );
208 $column->set_contexts( [ $replacement ] );
209 }
210
211 return $column;
212 }
213
214 $replacements = $this->get_replace_positions( $row_value );
215
216 $filter = new Filter\Type\Filter_String(
217 [
218 'value' => $this->search_value,
219 'logic' => 'contains',
220 'flags' => $this->search_flags->to_json(),
221 ], $this->schema
222 );
223 $matches = $filter->get_match( $source, new Action\Type\Nothing(), 'contains', $this->search_value, $row_value, $this->search_flags, $replacements );
224
225 // If we replaced anything then update the context with our new matches, otherwise just return whatever we have
226 if ( ( count( $matches ) === 1 && ! $matches[0] instanceof Context\Type\Value ) || count( $matches ) > 1 ) {
227 $column->set_contexts( $matches );
228 }
229
230 return $column;
231 }
232
233 // Replace a specific position
234 $replacements = $this->get_replace_positions( $row_value );
235 $contexts = Search\Text::get_all( $this->search_value, $this->search_flags, $replacements, $row_value );
236
237 foreach ( $contexts as $context ) {
238 $match = $context->get_match_at_position( $this->pos_id );
239
240 if ( is_object( $match ) ) {
241 $value = apply_filters( 'searchregex_text', $match->replace_at_position( $row_value ), $row_id, $row_value, $raw, $source->get_schema_item() );
242
243 // Need to replace the match with the result in the raw data
244 if ( $row_value !== $value ) {
245 $context = new Context\Type\Replace( $row_value );
246 $context->set_replacement( $value );
247 $column->set_contexts( [ $context ] );
248 }
249
250 return $column;
251 }
252 }
253
254 return $column;
255 }
256
257 /**
258 * Get replacement value
259 *
260 * @return string|null
261 */
262 public function get_replace_value() {
263 return $this->replace_value;
264 }
265 }
266