| @@ -47,13 +47,86 @@ | ||
| 47 | 47 | { |
| 48 | 48 | $output = []; |
| 49 | 49 | |
| 50 | 50 | if (isset($group['fields'])) { |
| 51 | + | |
| 52 | + // find files matching ._mapped._index | |
| 53 | + | |
| 54 | + $maps = []; | |
| 51 | 55 | foreach ($group['fields'] as $field_key => $field_value) { |
| 52 | - $output[$field_key] = $this->query_string($field_value); | |
| 56 | + | |
| 57 | + $matches = []; | |
| 58 | + if ( | |
| 59 | + // post.post_name._mapped._index | |
| 60 | + // custom_fields.0.value._mapped._index | |
| 61 | + preg_match('/^(.*?)\._mapped\._index$/', $field_key, $matches) === 1 && | |
| 62 | + // ignore custom_fields.0._mapped._index old style FieldMapper | |
| 63 | + preg_match('/^custom_fields\.(?:[0-9]+)\._mapped\._index$/', $field_key) === 0 && | |
| 64 | + intval($field_value) > 0 | |
| 65 | + ) { | |
| 66 | + $maps[$matches[1]] = $field_value; | |
| 67 | + } else { | |
| 68 | + $output[$field_key] = $this->query_string($field_value); | |
| 69 | + } | |
| 53 | 70 | } |
| 54 | 71 | } |
| 55 | 72 | |
| 73 | + if (!empty($maps)) { | |
| 74 | + foreach ($maps as $field_key => $field_rows) { | |
| 75 | + | |
| 76 | + $data = []; | |
| 77 | + $delimiter = false; | |
| 78 | + | |
| 79 | + if (isset($output[$field_key . '._mapped._delimiter']) && !empty($output[$field_key . '._mapped._delimiter'])) { | |
| 80 | + | |
| 81 | + // get delimiter from FieldMap delimiter field, this has priority | |
| 82 | + $delimiter = $output[$field_key . '._mapped._delimiter']; | |
| 83 | + } else { | |
| 84 | + | |
| 85 | + // get delimiter from parent section settings. e.g. taxonomies and attachments | |
| 86 | + $lastPos = strrpos($field_key, '.'); | |
| 87 | + if ($lastPos !== false) { | |
| 88 | + $tmp = substr($field_key, 0, $lastPos); | |
| 89 | + if (isset($output[$tmp . '.settings._delimiter'])) { | |
| 90 | + $delimiter = !empty($output[$tmp . '.settings._delimiter']) ? $output[$tmp . '.settings._delimiter'] : ','; | |
| 91 | + } | |
| 92 | + } | |
| 93 | + } | |
| 94 | + | |
| 95 | + foreach ($output as $item_key => $item_value) { | |
| 96 | + $matches = []; | |
| 97 | + if (preg_match('/^' . $field_key . '\._mapped\.([0-9]+)\.(.*?)$/', $item_key, $matches) === 1) { | |
| 98 | + | |
| 99 | + // row is to high | |
| 100 | + if (intval($matches[1]) >= intval($field_rows)) { | |
| 101 | + | |
| 102 | + unset($output[$matches[0]]); | |
| 103 | + continue; | |
| 104 | + } | |
| 105 | + | |
| 106 | + if (!isset($data[$matches[1]])) { | |
| 107 | + $data[$matches[1]] = []; | |
| 108 | + } | |
| 109 | + | |
| 110 | + $data[$matches[1]][$matches[2]] = $item_value; | |
| 111 | + | |
| 112 | + unset($output[$matches[0]]); | |
| 113 | + } | |
| 114 | + } | |
| 115 | + | |
| 116 | + if (!empty($delimiter)) { | |
| 117 | + | |
| 118 | + $field_parts = explode($delimiter, $output[$field_key]); | |
| 119 | + foreach ($field_parts as $k => $field_part) { | |
| 120 | + $field_parts[$k] = $this->map_field_data($field_part, array_values($data)); | |
| 121 | + } | |
| 122 | + $output[$field_key] = implode($delimiter, $field_parts); | |
| 123 | + } else { | |
| 124 | + $output[$field_key] = $this->map_field_data($output[$field_key], array_values($data)); | |
| 125 | + } | |
| 126 | + } | |
| 127 | + } | |
| 128 | + | |
| 56 | 129 | return $output; |
| 57 | 130 | } |
| 58 | 131 | |
| 59 | 132 | /** |
| @@ -64,13 +137,12 @@ | ||
| 64 | 137 | * @return string |
| 65 | 138 | */ |
| 66 | 139 | public function query_string($query) |
| 67 | 140 | { |
| 68 | - | |
| 69 | - $output = preg_replace_callback('/{(.*?)}/', array($this, 'query_matches'), $query); | |
| 70 | - $output = $this->handle_custom_methods($output); | |
| 71 | - | |
| 72 | - return $output; | |
| 141 | + // Parse [iwp:method(...)] before substituting {column} values, otherwise | |
| 142 | + // parentheses in the data (e.g. Excel =Hyperlink("url")) can close a | |
| 143 | + // malformed mapping that is missing ")". | |
| 144 | + return $this->interpolate_braces($this->handle_custom_methods($query)); | |
| 73 | 145 | } |
| 74 | 146 | |
| 75 | 147 | public function query_matches($matches) |
| 76 | 148 | { |
| @@ -89,33 +161,227 @@ | ||
| 89 | 161 | } |
| 90 | 162 | |
| 91 | 163 | public function handle_custom_methods($input) |
| 92 | 164 | { |
| 93 | - // m: Multiline modifier | |
| 94 | - // s: matches all characters including newlines | |
| 95 | - // U: non-greedy matching | |
| 96 | - $input = preg_replace_callback('/\[([\w]+)\((.*?)\)\]/msU', function ($matches) { | |
| 165 | + // Prefixed [iwp:method(...)] to avoid colliding with shortcodes / Gutenberg content. | |
| 166 | + if (!is_string($input) || $input === '' || strpos($input, '[iwp:') === false) { | |
| 167 | + return $input; | |
| 168 | + } | |
| 97 | 169 | |
| 98 | - $method = $matches[1]; | |
| 170 | + $offset = 0; | |
| 171 | + $output = ''; | |
| 172 | + $length = strlen($input); | |
| 99 | 173 | |
| 100 | - $result = []; | |
| 101 | - $args = []; | |
| 174 | + while (($start = strpos($input, '[iwp:', $offset)) !== false) { | |
| 175 | + $output .= substr($input, $offset, $start - $offset); | |
| 102 | 176 | |
| 103 | - // Dont split comma's if they are inside a double quote | |
| 104 | - if (preg_match_all('/(?:".*?"|[^",\s]+)(?=\s*,|\s*$)/s', $matches[2], $result) > 0) { | |
| 105 | - $args = $result[0]; | |
| 106 | - foreach ($args as &$arg) { | |
| 177 | + $parsed = $this->parse_custom_method_at($input, $start, $length); | |
| 178 | + if ($parsed === null) { | |
| 179 | + $output .= '[iwp:'; | |
| 180 | + $offset = $start + 5; | |
| 181 | + continue; | |
| 182 | + } | |
| 107 | 183 | |
| 108 | - // Strip commas from start and end of string | |
| 109 | - $arg = preg_replace('/^(\'(.*)\'|"(.*)")$/s', '$2$3', $arg); | |
| 184 | + $output .= $parsed[1]; | |
| 185 | + $offset = $parsed[0]; | |
| 186 | + } | |
| 187 | + | |
| 188 | + return $output . substr($input, $offset); | |
| 189 | + } | |
| 190 | + | |
| 191 | + /** | |
| 192 | + * Substitute {column} / {xpath} selectors. | |
| 193 | + * | |
| 194 | + * @param string $query | |
| 195 | + * @return string | |
| 196 | + */ | |
| 197 | + private function interpolate_braces($query) | |
| 198 | + { | |
| 199 | + if (!is_string($query) || $query === '' || strpos($query, '{') === false) { | |
| 200 | + return $query; | |
| 201 | + } | |
| 202 | + | |
| 203 | + return preg_replace_callback('/{(.*?)}/', array($this, 'query_matches'), $query); | |
| 204 | + } | |
| 205 | + | |
| 206 | + /** | |
| 207 | + * @param string $input | |
| 208 | + * @param int $start | |
| 209 | + * @param int $length | |
| 210 | + * @return array{0:int,1:string}|null End offset and replacement, or null if not a complete call. | |
| 211 | + */ | |
| 212 | + private function parse_custom_method_at($input, $start, $length) | |
| 213 | + { | |
| 214 | + $i = $start + 5; | |
| 215 | + $name_len = strspn($input, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_', $i); | |
| 216 | + if ($name_len < 1) { | |
| 217 | + return null; | |
| 218 | + } | |
| 219 | + | |
| 220 | + $method = substr($input, $i, $name_len); | |
| 221 | + $i += $name_len; | |
| 222 | + if ($i >= $length || $input[$i] !== '(') { | |
| 223 | + return null; | |
| 224 | + } | |
| 225 | + $i++; | |
| 226 | + | |
| 227 | + $extracted = $this->extract_balanced_method_args($input, $i, $length); | |
| 228 | + if ($extracted === null) { | |
| 229 | + return null; | |
| 230 | + } | |
| 231 | + | |
| 232 | + list($raw_args, $after_paren) = $extracted; | |
| 233 | + if ($after_paren >= $length || $input[$after_paren] !== ']') { | |
| 234 | + return null; | |
| 235 | + } | |
| 236 | + | |
| 237 | + $end = $after_paren + 1; | |
| 238 | + $original = substr($input, $start, $end - $start); | |
| 239 | + $args = $this->split_custom_method_args($raw_args); | |
| 240 | + foreach ($args as &$arg) { | |
| 241 | + $arg = $this->interpolate_braces($arg); | |
| 242 | + } | |
| 243 | + unset($arg); | |
| 244 | + | |
| 245 | + if (is_callable($method)) { | |
| 246 | + return array($end, call_user_func_array($method, $args)); | |
| 247 | + } | |
| 248 | + | |
| 249 | + return array($end, $original); | |
| 250 | + } | |
| 251 | + | |
| 252 | + /** | |
| 253 | + * Read until the method's closing ")", ignoring parentheses inside quotes. | |
| 254 | + * | |
| 255 | + * @param string $input | |
| 256 | + * @param int $start | |
| 257 | + * @param int $length | |
| 258 | + * @return array{0:string,1:int}|null Raw args and offset after ")", or null if unterminated. | |
| 259 | + */ | |
| 260 | + private function extract_balanced_method_args($input, $start, $length) | |
| 261 | + { | |
| 262 | + $depth = 1; | |
| 263 | + $quote = null; | |
| 264 | + for ($i = $start; $i < $length; $i++) { | |
| 265 | + $ch = $input[$i]; | |
| 266 | + if ($quote !== null) { | |
| 267 | + if ($ch === $quote) { | |
| 268 | + $quote = null; | |
| 110 | 269 | } |
| 270 | + continue; | |
| 111 | 271 | } |
| 272 | + if ($ch === '"' || $ch === "'") { | |
| 273 | + $quote = $ch; | |
| 274 | + continue; | |
| 275 | + } | |
| 276 | + if ($ch === '(') { | |
| 277 | + $depth++; | |
| 278 | + continue; | |
| 279 | + } | |
| 280 | + if ($ch === ')') { | |
| 281 | + $depth--; | |
| 282 | + if ($depth === 0) { | |
| 283 | + return array(substr($input, $start, $i - $start), $i + 1); | |
| 284 | + } | |
| 285 | + } | |
| 286 | + } | |
| 112 | 287 | |
| 113 | - if (is_callable($method)) { | |
| 114 | - return call_user_func_array($method, $args); | |
| 288 | + return null; | |
| 289 | + } | |
| 290 | + | |
| 291 | + /** | |
| 292 | + * @param string $raw_args | |
| 293 | + * @return array | |
| 294 | + */ | |
| 295 | + private function split_custom_method_args($raw_args) | |
| 296 | + { | |
| 297 | + $args = []; | |
| 298 | + if ($raw_args === '') { | |
| 299 | + return $args; | |
| 300 | + } | |
| 301 | + | |
| 302 | + // Dont split comma's if they are inside a double quote | |
| 303 | + if (preg_match_all('/(?:".*?"|[^",\s]+)(?=\s*,|\s*$)/s', $raw_args, $result) > 0) { | |
| 304 | + $args = $result[0]; | |
| 305 | + foreach ($args as &$arg) { | |
| 306 | + // Strip quotes from start and end of string | |
| 307 | + $arg = preg_replace('/^(\'(.*)\'|"(.*)")$/s', '$2$3', $arg); | |
| 115 | 308 | } |
| 309 | + unset($arg); | |
| 310 | + } | |
| 116 | 311 | |
| 117 | - return $matches[0]; | |
| 118 | - }, $input); | |
| 312 | + return $args; | |
| 313 | + } | |
| 314 | + | |
| 315 | + public function map_field_data($input, $map) | |
| 316 | + { | |
| 317 | + foreach ($map as $map_data) { | |
| 318 | + | |
| 319 | + $condition = isset($map_data['_condition']) && !empty($map_data['_condition']) ? $map_data['_condition'] : 'equal'; | |
| 320 | + $key = isset($map_data['key']) ? $map_data['key'] : ''; | |
| 321 | + $output = isset($map_data['value']) ? $map_data['value'] : ''; | |
| 322 | + | |
| 323 | + switch ($condition) { | |
| 324 | + | |
| 325 | + case 'gt': | |
| 326 | + $left = intval($input); | |
| 327 | + $right = intval($key); | |
| 328 | + if ($left > $right) { | |
| 329 | + return $output; | |
| 330 | + } | |
| 331 | + break; | |
| 332 | + case 'gte': | |
| 333 | + $left = intval($input); | |
| 334 | + $right = intval($key); | |
| 335 | + if ($left >= $right) { | |
| 336 | + return $output; | |
| 337 | + } | |
| 338 | + break; | |
| 339 | + case 'lt': | |
| 340 | + $left = intval($input); | |
| 341 | + $right = intval($key); | |
| 342 | + if ($left < $right) { | |
| 343 | + return $output; | |
| 344 | + } | |
| 345 | + break; | |
| 346 | + case 'lte': | |
| 347 | + $left = intval($input); | |
| 348 | + $right = intval($key); | |
| 349 | + if ($left <= $right) { | |
| 350 | + return $output; | |
| 351 | + } | |
| 352 | + break; | |
| 353 | + case 'contains': | |
| 354 | + if (stripos($input, trim($key)) !== false) { | |
| 355 | + return $output; | |
| 356 | + } | |
| 357 | + break; | |
| 358 | + case 'in': | |
| 359 | + if (in_array($input, explode(',', $key))) { | |
| 360 | + return $output; | |
| 361 | + } | |
| 362 | + break; | |
| 363 | + case 'not-equal': | |
| 364 | + if (trim($key) !== trim($input)) { | |
| 365 | + return $output; | |
| 366 | + } | |
| 367 | + break; | |
| 368 | + case 'not-contains': | |
| 369 | + if (stripos($input, trim($key)) === false) { | |
| 370 | + return $output; | |
| 371 | + } | |
| 372 | + break; | |
| 373 | + case 'not-in': | |
| 374 | + if (!in_array($input, explode(',', $key))) { | |
| 375 | + return $output; | |
| 376 | + } | |
| 377 | + break; | |
| 378 | + default: | |
| 379 | + if (trim($key) === trim($input)) { | |
| 380 | + return $output; | |
| 381 | + } | |
| 382 | + break; | |
| 383 | + } | |
| 384 | + } | |
| 119 | 385 | return $input; |
| 120 | 386 | } |
| 121 | 387 | } |