1ce42304aSGreg Roach<?php 2ce42304aSGreg Roach 3ce42304aSGreg Roach/** 4ce42304aSGreg Roach * webtrees: online genealogy 5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team 6ce42304aSGreg Roach * This program is free software: you can redistribute it and/or modify 7ce42304aSGreg Roach * it under the terms of the GNU General Public License as published by 8ce42304aSGreg Roach * the Free Software Foundation, either version 3 of the License, or 9ce42304aSGreg Roach * (at your option) any later version. 10ce42304aSGreg Roach * This program is distributed in the hope that it will be useful, 11ce42304aSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 12ce42304aSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13ce42304aSGreg Roach * GNU General Public License for more details. 14ce42304aSGreg Roach * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16ce42304aSGreg Roach */ 17ce42304aSGreg Roach 18ce42304aSGreg Roachdeclare(strict_types=1); 19ce42304aSGreg Roach 20ce42304aSGreg Roachnamespace Fisharebest\Webtrees\Module; 21ce42304aSGreg Roach 226f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB; 23ce42304aSGreg Roachuse Fisharebest\Webtrees\Family; 24ce42304aSGreg Roachuse Fisharebest\Webtrees\GedcomRecord; 2581b729d3SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException; 26ce42304aSGreg Roachuse Fisharebest\Webtrees\I18N; 27ce42304aSGreg Roachuse Fisharebest\Webtrees\Individual; 282da2d0c9SGreg Roachuse Fisharebest\Webtrees\Location; 29ce42304aSGreg Roachuse Fisharebest\Webtrees\Media; 30ce42304aSGreg Roachuse Fisharebest\Webtrees\Note; 31ce42304aSGreg Roachuse Fisharebest\Webtrees\Repository; 32ce42304aSGreg Roachuse Fisharebest\Webtrees\Services\DataFixService; 33ce42304aSGreg Roachuse Fisharebest\Webtrees\Source; 34ce42304aSGreg Roachuse Fisharebest\Webtrees\Submitter; 35ce42304aSGreg Roachuse Fisharebest\Webtrees\Tree; 36ce42304aSGreg Roachuse Illuminate\Database\Query\Builder; 37ce42304aSGreg Roachuse Illuminate\Support\Collection; 38ce42304aSGreg Roachuse Throwable; 39ce42304aSGreg Roach 40ce42304aSGreg Roachuse function addcslashes; 41ce42304aSGreg Roachuse function asort; 42ce42304aSGreg Roachuse function preg_match; 43ce42304aSGreg Roachuse function preg_quote; 44ce42304aSGreg Roachuse function preg_replace; 45ce42304aSGreg Roachuse function view; 46ce42304aSGreg Roach 47ce42304aSGreg Roach/** 48ce42304aSGreg Roach * Class FixSearchAndReplace 49ce42304aSGreg Roach */ 50ce42304aSGreg Roachclass FixSearchAndReplace extends AbstractModule implements ModuleDataFixInterface 51ce42304aSGreg Roach{ 52ce42304aSGreg Roach use ModuleDataFixTrait; 53ce42304aSGreg Roach 54ce42304aSGreg Roach // A regular expression that never matches. 55ce42304aSGreg Roach private const INVALID_REGEX = '/(?!)/'; 56ce42304aSGreg Roach 5743f2f523SGreg Roach private DataFixService $data_fix_service; 58ce42304aSGreg Roach 59ce42304aSGreg Roach /** 60ce42304aSGreg Roach * @param DataFixService $data_fix_service 61ce42304aSGreg Roach */ 62ce42304aSGreg Roach public function __construct(DataFixService $data_fix_service) 63ce42304aSGreg Roach { 64ce42304aSGreg Roach $this->data_fix_service = $data_fix_service; 65ce42304aSGreg Roach } 66ce42304aSGreg Roach 67ce42304aSGreg Roach /** 68ce42304aSGreg Roach * How should this module be identified in the control panel, etc.? 69ce42304aSGreg Roach * 70ce42304aSGreg Roach * @return string 71ce42304aSGreg Roach */ 72ce42304aSGreg Roach public function title(): string 73ce42304aSGreg Roach { 74ce42304aSGreg Roach /* I18N: Name of a module */ 75ce42304aSGreg Roach return I18N::translate('Search and replace'); 76ce42304aSGreg Roach } 77ce42304aSGreg Roach 78ce42304aSGreg Roach public function description(): string 79ce42304aSGreg Roach { 80ce42304aSGreg Roach /* I18N: Description of a “Data fix” module */ 81ce42304aSGreg Roach return I18N::translate('Search and replace text, using simple searches or advanced pattern matching.'); 82ce42304aSGreg Roach } 83ce42304aSGreg Roach 84ce42304aSGreg Roach /** 85ce42304aSGreg Roach * Options form. 86ce42304aSGreg Roach * 87ce42304aSGreg Roach * @param Tree $tree 88ce42304aSGreg Roach * 89ce42304aSGreg Roach * @return string 90ce42304aSGreg Roach */ 91ce42304aSGreg Roach public function fixOptions(Tree $tree): string 92ce42304aSGreg Roach { 93ce42304aSGreg Roach $methods = [ 94ce42304aSGreg Roach 'exact' => I18N::translate('Match the exact text, even if it occurs in the middle of a word.'), 95ce42304aSGreg Roach 'words' => I18N::translate('Match the exact text, unless it occurs in the middle of a word.'), 96ce42304aSGreg Roach 'wildcards' => I18N::translate('Use a “?” to match a single character, use “*” to match zero or more characters.'), 97ad3143ccSGreg Roach /* I18N: https://en.wikipedia.org/wiki/Regular_expression */ 982ab7d347SGreg Roach 'regex' => I18N::translate('Regular expression'), 99ce42304aSGreg Roach ]; 100ce42304aSGreg Roach 101ce42304aSGreg Roach $types = [ 102ce42304aSGreg Roach Family::RECORD_TYPE => I18N::translate('Families'), 103ce42304aSGreg Roach Individual::RECORD_TYPE => I18N::translate('Individuals'), 1042da2d0c9SGreg Roach Location::RECORD_TYPE => I18N::translate('Locations'), 105ce42304aSGreg Roach Media::RECORD_TYPE => I18N::translate('Media objects'), 106ce42304aSGreg Roach Note::RECORD_TYPE => I18N::translate('Notes'), 107ce42304aSGreg Roach Repository::RECORD_TYPE => I18N::translate('Repositories'), 108ce42304aSGreg Roach Source::RECORD_TYPE => I18N::translate('Sources'), 109ce42304aSGreg Roach Submitter::RECORD_TYPE => I18N::translate('Submitters'), 110ce42304aSGreg Roach ]; 111ce42304aSGreg Roach 112ce42304aSGreg Roach asort($types); 113ce42304aSGreg Roach 114ce42304aSGreg Roach return view('modules/fix-search-and-replace/options', [ 115ce42304aSGreg Roach 'default_method' => 'exact', 116ce42304aSGreg Roach 'default_type' => Individual::RECORD_TYPE, 117ce42304aSGreg Roach 'methods' => $methods, 118ce42304aSGreg Roach 'types' => $types, 119ce42304aSGreg Roach ]); 120ce42304aSGreg Roach } 121ce42304aSGreg Roach 122ce42304aSGreg Roach /** 123ce42304aSGreg Roach * A list of all records that need examining. This may include records 124ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 125ce42304aSGreg Roach * 126ce42304aSGreg Roach * @param Tree $tree 127ce42304aSGreg Roach * @param array<string,string> $params 128ce42304aSGreg Roach * 12936779af1SGreg Roach * @return Collection<int,string>|null 130ce42304aSGreg Roach */ 1311ff45046SGreg Roach protected function familiesToFix(Tree $tree, array $params): Collection|null 132ce42304aSGreg Roach { 133748dbe15SGreg Roach if ($params['type'] !== Family::RECORD_TYPE || $params['search-for'] === '') { 134ce42304aSGreg Roach return null; 135ce42304aSGreg Roach } 136ce42304aSGreg Roach 137ce42304aSGreg Roach $query = DB::table('families')->where('f_file', '=', $tree->id()); 138ce42304aSGreg Roach $this->recordQuery($query, 'f_gedcom', $params); 139ce42304aSGreg Roach 140ce42304aSGreg Roach return $query->pluck('f_id'); 141ce42304aSGreg Roach } 142ce42304aSGreg Roach 143ce42304aSGreg Roach /** 144ce42304aSGreg Roach * A list of all records that need examining. This may include records 145ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 146ce42304aSGreg Roach * 147ce42304aSGreg Roach * @param Tree $tree 148ce42304aSGreg Roach * @param array<string,string> $params 149ce42304aSGreg Roach * 15036779af1SGreg Roach * @return Collection<int,string>|null 151ce42304aSGreg Roach */ 1521ff45046SGreg Roach protected function individualsToFix(Tree $tree, array $params): Collection|null 153ce42304aSGreg Roach { 154748dbe15SGreg Roach if ($params['type'] !== Individual::RECORD_TYPE || $params['search-for'] === '') { 155ce42304aSGreg Roach return null; 156ce42304aSGreg Roach } 157ce42304aSGreg Roach 158ce42304aSGreg Roach $query = DB::table('individuals') 159ce42304aSGreg Roach ->where('i_file', '=', $tree->id()); 160ce42304aSGreg Roach 161ce42304aSGreg Roach $this->recordQuery($query, 'i_gedcom', $params); 162ce42304aSGreg Roach 163ce42304aSGreg Roach return $query->pluck('i_id'); 164ce42304aSGreg Roach } 165ce42304aSGreg Roach 166ce42304aSGreg Roach /** 167ce42304aSGreg Roach * A list of all records that need examining. This may include records 168ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 169ce42304aSGreg Roach * 170ce42304aSGreg Roach * @param Tree $tree 171ce42304aSGreg Roach * @param array<string,string> $params 172ce42304aSGreg Roach * 17336779af1SGreg Roach * @return Collection<int,string>|null 174ce42304aSGreg Roach */ 1751ff45046SGreg Roach protected function locationsToFix(Tree $tree, array $params): Collection|null 17692657c8aSGreg Roach { 177748dbe15SGreg Roach if ($params['type'] !== Location::RECORD_TYPE || $params['search-for'] === '') { 17892657c8aSGreg Roach return null; 17992657c8aSGreg Roach } 18092657c8aSGreg Roach 18192657c8aSGreg Roach $query = DB::table('other') 18292657c8aSGreg Roach ->where('o_file', '=', $tree->id()) 18392657c8aSGreg Roach ->where('o_type', '=', Location::RECORD_TYPE); 18492657c8aSGreg Roach 18592657c8aSGreg Roach $this->recordQuery($query, 'o_gedcom', $params); 18692657c8aSGreg Roach 18792657c8aSGreg Roach return $query->pluck('o_id'); 18892657c8aSGreg Roach } 18992657c8aSGreg Roach 19092657c8aSGreg Roach /** 19192657c8aSGreg Roach * A list of all records that need examining. This may include records 19292657c8aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 19392657c8aSGreg Roach * 19492657c8aSGreg Roach * @param Tree $tree 19592657c8aSGreg Roach * @param array<string,string> $params 19692657c8aSGreg Roach * 19736779af1SGreg Roach * @return Collection<int,string>|null 19892657c8aSGreg Roach */ 1991ff45046SGreg Roach protected function mediaToFix(Tree $tree, array $params): Collection|null 200ce42304aSGreg Roach { 201748dbe15SGreg Roach if ($params['type'] !== Media::RECORD_TYPE || $params['search-for'] === '') { 202ce42304aSGreg Roach return null; 203ce42304aSGreg Roach } 204ce42304aSGreg Roach 205ce42304aSGreg Roach $query = DB::table('media') 206ce42304aSGreg Roach ->where('m_file', '=', $tree->id()); 207ce42304aSGreg Roach 208ce42304aSGreg Roach $this->recordQuery($query, 'm_gedcom', $params); 209ce42304aSGreg Roach 210ce42304aSGreg Roach return $query->pluck('m_id'); 211ce42304aSGreg Roach } 212ce42304aSGreg Roach 213ce42304aSGreg Roach /** 214ce42304aSGreg Roach * A list of all records that need examining. This may include records 215ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 216ce42304aSGreg Roach * 217ce42304aSGreg Roach * @param Tree $tree 218ce42304aSGreg Roach * @param array<string,string> $params 219ce42304aSGreg Roach * 22036779af1SGreg Roach * @return Collection<int,string>|null 221ce42304aSGreg Roach */ 2221ff45046SGreg Roach protected function notesToFix(Tree $tree, array $params): Collection|null 223ce42304aSGreg Roach { 224748dbe15SGreg Roach if ($params['type'] !== Note::RECORD_TYPE || $params['search-for'] === '') { 225ce42304aSGreg Roach return null; 226ce42304aSGreg Roach } 227ce42304aSGreg Roach 228ce42304aSGreg Roach $query = DB::table('other') 229ce42304aSGreg Roach ->where('o_file', '=', $tree->id()) 230ce42304aSGreg Roach ->where('o_type', '=', Note::RECORD_TYPE); 231ce42304aSGreg Roach 232ce42304aSGreg Roach $this->recordQuery($query, 'o_gedcom', $params); 233ce42304aSGreg Roach 234ce42304aSGreg Roach return $query->pluck('o_id'); 235ce42304aSGreg Roach } 236ce42304aSGreg Roach 237ce42304aSGreg Roach /** 238ce42304aSGreg Roach * A list of all records that need examining. This may include records 239ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 240ce42304aSGreg Roach * 241ce42304aSGreg Roach * @param Tree $tree 242ce42304aSGreg Roach * @param array<string,string> $params 243ce42304aSGreg Roach * 24436779af1SGreg Roach * @return Collection<int,string>|null 245ce42304aSGreg Roach */ 2461ff45046SGreg Roach protected function repositoriesToFix(Tree $tree, array $params): Collection|null 247ce42304aSGreg Roach { 248748dbe15SGreg Roach if ($params['type'] !== Repository::RECORD_TYPE || $params['search-for'] === '') { 249ce42304aSGreg Roach return null; 250ce42304aSGreg Roach } 251ce42304aSGreg Roach 252ce42304aSGreg Roach $query = DB::table('other') 253ce42304aSGreg Roach ->where('o_file', '=', $tree->id()) 254ce42304aSGreg Roach ->where('o_type', '=', Repository::RECORD_TYPE); 255ce42304aSGreg Roach 256ce42304aSGreg Roach $this->recordQuery($query, 'o_gedcom', $params); 257ce42304aSGreg Roach 258ce42304aSGreg Roach return $query->pluck('o_id'); 259ce42304aSGreg Roach } 260ce42304aSGreg Roach 261ce42304aSGreg Roach /** 262ce42304aSGreg Roach * A list of all records that need examining. This may include records 263ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 264ce42304aSGreg Roach * 265ce42304aSGreg Roach * @param Tree $tree 266ce42304aSGreg Roach * @param array<string,string> $params 267ce42304aSGreg Roach * 26836779af1SGreg Roach * @return Collection<int,string>|null 269ce42304aSGreg Roach */ 2701ff45046SGreg Roach protected function sourcesToFix(Tree $tree, array $params): Collection|null 271ce42304aSGreg Roach { 272748dbe15SGreg Roach if ($params['type'] !== Source::RECORD_TYPE || $params['search-for'] === '') { 273ce42304aSGreg Roach return null; 274ce42304aSGreg Roach } 275ce42304aSGreg Roach 2767684867eSGreg Roach $query = $this->sourcesToFixQuery($tree, $params); 277ce42304aSGreg Roach 278ce42304aSGreg Roach $this->recordQuery($query, 's_gedcom', $params); 279ce42304aSGreg Roach 280ce42304aSGreg Roach return $query->pluck('s_id'); 281ce42304aSGreg Roach } 282ce42304aSGreg Roach 283ce42304aSGreg Roach /** 284ce42304aSGreg Roach * A list of all records that need examining. This may include records 285ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 286ce42304aSGreg Roach * 287ce42304aSGreg Roach * @param Tree $tree 288ce42304aSGreg Roach * @param array<string,string> $params 289ce42304aSGreg Roach * 29036779af1SGreg Roach * @return Collection<int,string>|null 291ce42304aSGreg Roach */ 2921ff45046SGreg Roach protected function submittersToFix(Tree $tree, array $params): Collection|null 293ce42304aSGreg Roach { 294748dbe15SGreg Roach if ($params['type'] !== Submitter::RECORD_TYPE || $params['search-for'] === '') { 295ce42304aSGreg Roach return null; 296ce42304aSGreg Roach } 297ce42304aSGreg Roach 2987684867eSGreg Roach $query = $this->submittersToFixQuery($tree, $params); 299ce42304aSGreg Roach 300ce42304aSGreg Roach $this->recordQuery($query, 'o_gedcom', $params); 301ce42304aSGreg Roach 302ce42304aSGreg Roach return $query->pluck('o_id'); 303ce42304aSGreg Roach } 304ce42304aSGreg Roach 305ce42304aSGreg Roach /** 306ce42304aSGreg Roach * Does a record need updating? 307ce42304aSGreg Roach * 308ce42304aSGreg Roach * @param GedcomRecord $record 309ce42304aSGreg Roach * @param array<string,string> $params 310ce42304aSGreg Roach * 311ce42304aSGreg Roach * @return bool 312ce42304aSGreg Roach */ 313ce42304aSGreg Roach public function doesRecordNeedUpdate(GedcomRecord $record, array $params): bool 314ce42304aSGreg Roach { 315ce42304aSGreg Roach return preg_match($this->createRegex($params), $record->gedcom()) === 1; 316ce42304aSGreg Roach } 317ce42304aSGreg Roach 318ce42304aSGreg Roach /** 319ce42304aSGreg Roach * Show the changes we would make 320ce42304aSGreg Roach * 321ce42304aSGreg Roach * @param GedcomRecord $record 322ce42304aSGreg Roach * @param array<string,string> $params 323ce42304aSGreg Roach * 324ce42304aSGreg Roach * @return string 325ce42304aSGreg Roach */ 326ce42304aSGreg Roach public function previewUpdate(GedcomRecord $record, array $params): string 327ce42304aSGreg Roach { 328ce42304aSGreg Roach $old = $record->gedcom(); 329ce42304aSGreg Roach $new = $this->updateGedcom($record, $params); 330ce42304aSGreg Roach 331ce42304aSGreg Roach return $this->data_fix_service->gedcomDiff($record->tree(), $old, $new); 332ce42304aSGreg Roach } 333ce42304aSGreg Roach 334ce42304aSGreg Roach /** 335ce42304aSGreg Roach * Fix a record 336ce42304aSGreg Roach * 337ce42304aSGreg Roach * @param GedcomRecord $record 338ce42304aSGreg Roach * @param array<string,string> $params 339ce42304aSGreg Roach * 340ce42304aSGreg Roach * @return void 341ce42304aSGreg Roach */ 342ce42304aSGreg Roach public function updateRecord(GedcomRecord $record, array $params): void 343ce42304aSGreg Roach { 344ce42304aSGreg Roach $record->updateRecord($this->updateGedcom($record, $params), false); 345ce42304aSGreg Roach } 346ce42304aSGreg Roach 347ce42304aSGreg Roach /** 348ce42304aSGreg Roach * @param GedcomRecord $record 349ce42304aSGreg Roach * @param array<string,string> $params 350ce42304aSGreg Roach * 351ce42304aSGreg Roach * @return string 352ce42304aSGreg Roach */ 353ce42304aSGreg Roach private function updateGedcom(GedcomRecord $record, array $params): string 354ce42304aSGreg Roach { 355ce42304aSGreg Roach // Allow "\n" to indicate a line-feed in replacement text. 356ce42304aSGreg Roach // Back-references such as $1, $2 are handled automatically. 357748dbe15SGreg Roach $replace = strtr($params['replace-with'], ['\n' => "\n"]); 358ce42304aSGreg Roach 359ce42304aSGreg Roach $regex = $this->createRegex($params); 360ce42304aSGreg Roach 361ce42304aSGreg Roach return preg_replace($regex, $replace, $record->gedcom()); 362ce42304aSGreg Roach } 363ce42304aSGreg Roach 364ce42304aSGreg Roach /** 365ce42304aSGreg Roach * Create a regular expression from the search pattern. 366ce42304aSGreg Roach * 367ce42304aSGreg Roach * @param array<string,string> $params 368ce42304aSGreg Roach * 369ce42304aSGreg Roach * @return string 370ce42304aSGreg Roach */ 371ce42304aSGreg Roach private function createRegex(array $params): string 372ce42304aSGreg Roach { 373748dbe15SGreg Roach $search = $params['search-for']; 374ce42304aSGreg Roach $method = $params['method']; 375ce42304aSGreg Roach $case = $params['case']; 376ce42304aSGreg Roach 377ce42304aSGreg Roach switch ($method) { 378ce42304aSGreg Roach case 'exact': 379681f0c68SGreg Roach return '/' . preg_quote($search, '/') . '/u' . $case; 380ce42304aSGreg Roach 381ce42304aSGreg Roach case 'words': 382681f0c68SGreg Roach return '/\b' . preg_quote($search, '/') . '\b/u' . $case; 383ce42304aSGreg Roach 384ce42304aSGreg Roach case 'wildcards': 385681f0c68SGreg Roach return '/\b' . strtr(preg_quote($search, '/'), ['\*' => '.*', '\?' => '.']) . '\b/u' . $case; 386ce42304aSGreg Roach 387ce42304aSGreg Roach case 'regex': 388a4f494bbSGreg Roach $regex = '/' . addcslashes($search, '/') . '/u' . $case; 389ce42304aSGreg Roach 390ce42304aSGreg Roach try { 391ce42304aSGreg Roach // A valid regex on an empty string returns zero. 392ce42304aSGreg Roach // An invalid regex on an empty string returns false and throws a warning. 393ce42304aSGreg Roach preg_match($regex, ''); 39428d026adSGreg Roach } catch (Throwable) { 395ce42304aSGreg Roach $regex = self::INVALID_REGEX; 396ce42304aSGreg Roach } 397ce42304aSGreg Roach 398ce42304aSGreg Roach return $regex; 399ce42304aSGreg Roach } 400ce42304aSGreg Roach 401ce42304aSGreg Roach throw new HttpNotFoundException(); 402ce42304aSGreg Roach } 403ce42304aSGreg Roach 404ce42304aSGreg Roach /** 405ce42304aSGreg Roach * Create a regular expression from the search pattern. 406ce42304aSGreg Roach * 407ce42304aSGreg Roach * @param Builder $query 408ce42304aSGreg Roach * @param string $column 409ce42304aSGreg Roach * @param array<string,string> $params 410ce42304aSGreg Roach * 411ce42304aSGreg Roach * @return void 412ce42304aSGreg Roach */ 413ce42304aSGreg Roach private function recordQuery(Builder $query, string $column, array $params): void 414ce42304aSGreg Roach { 415748dbe15SGreg Roach $search = $params['search-for']; 416ce42304aSGreg Roach $method = $params['method']; 417b5961194SGreg Roach $like = '%' . addcslashes($search, '\\%_') . '%'; 418ce42304aSGreg Roach 419ce42304aSGreg Roach switch ($method) { 420ce42304aSGreg Roach case 'exact': 421ce42304aSGreg Roach case 'words': 422b5961194SGreg Roach $query->where($column, 'LIKE', $like); 423ce42304aSGreg Roach break; 424ce42304aSGreg Roach 425ce42304aSGreg Roach case 'wildcards': 426ce42304aSGreg Roach $like = strtr($like, ['?' => '_', '*' => '%']); 427b5961194SGreg Roach $query->where($column, 'LIKE', $like); 428ce42304aSGreg Roach break; 429ce42304aSGreg Roach 430ce42304aSGreg Roach case 'regex': 4319a92a0c1SGreg Roach // Substituting newlines seems to be necessary on *some* versions 432e93aa0bdSGreg Roach // of MySQL (e.g. 5.7), and harmless on others (e.g. 8.0). 4339a92a0c1SGreg Roach $search = strtr($search, ['\n' => "\n"]); 4349a92a0c1SGreg Roach 435*52550490SGreg Roach $query->where($column, DB::regexOperator(), $search); 436ce42304aSGreg Roach break; 437ce42304aSGreg Roach } 438ce42304aSGreg Roach } 439ce42304aSGreg Roach} 440