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 /** 79ce42304aSGreg Roach * A sentence describing what this module does. 80ce42304aSGreg Roach * 81ce42304aSGreg Roach * @return string 82ce42304aSGreg Roach */ 83ce42304aSGreg Roach public function description(): string 84ce42304aSGreg Roach { 85ce42304aSGreg Roach /* I18N: Description of a “Data fix” module */ 86ce42304aSGreg Roach return I18N::translate('Search and replace text, using simple searches or advanced pattern matching.'); 87ce42304aSGreg Roach } 88ce42304aSGreg Roach 89ce42304aSGreg Roach /** 90ce42304aSGreg Roach * Options form. 91ce42304aSGreg Roach * 92ce42304aSGreg Roach * @param Tree $tree 93ce42304aSGreg Roach * 94ce42304aSGreg Roach * @return string 95ce42304aSGreg Roach */ 96ce42304aSGreg Roach public function fixOptions(Tree $tree): string 97ce42304aSGreg Roach { 98ce42304aSGreg Roach $methods = [ 99ce42304aSGreg Roach 'exact' => I18N::translate('Match the exact text, even if it occurs in the middle of a word.'), 100ce42304aSGreg Roach 'words' => I18N::translate('Match the exact text, unless it occurs in the middle of a word.'), 101ce42304aSGreg Roach 'wildcards' => I18N::translate('Use a “?” to match a single character, use “*” to match zero or more characters.'), 102ad3143ccSGreg Roach /* I18N: https://en.wikipedia.org/wiki/Regular_expression */ 1032ab7d347SGreg Roach 'regex' => I18N::translate('Regular expression'), 104ce42304aSGreg Roach ]; 105ce42304aSGreg Roach 106ce42304aSGreg Roach $types = [ 107ce42304aSGreg Roach Family::RECORD_TYPE => I18N::translate('Families'), 108ce42304aSGreg Roach Individual::RECORD_TYPE => I18N::translate('Individuals'), 1092da2d0c9SGreg Roach Location::RECORD_TYPE => I18N::translate('Locations'), 110ce42304aSGreg Roach Media::RECORD_TYPE => I18N::translate('Media objects'), 111ce42304aSGreg Roach Note::RECORD_TYPE => I18N::translate('Notes'), 112ce42304aSGreg Roach Repository::RECORD_TYPE => I18N::translate('Repositories'), 113ce42304aSGreg Roach Source::RECORD_TYPE => I18N::translate('Sources'), 114ce42304aSGreg Roach Submitter::RECORD_TYPE => I18N::translate('Submitters'), 115ce42304aSGreg Roach ]; 116ce42304aSGreg Roach 117ce42304aSGreg Roach asort($types); 118ce42304aSGreg Roach 119ce42304aSGreg Roach return view('modules/fix-search-and-replace/options', [ 120ce42304aSGreg Roach 'default_method' => 'exact', 121ce42304aSGreg Roach 'default_type' => Individual::RECORD_TYPE, 122ce42304aSGreg Roach 'methods' => $methods, 123ce42304aSGreg Roach 'types' => $types, 124ce42304aSGreg Roach ]); 125ce42304aSGreg Roach } 126ce42304aSGreg Roach 127ce42304aSGreg Roach /** 128ce42304aSGreg Roach * A list of all records that need examining. This may include records 129ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 130ce42304aSGreg Roach * 131ce42304aSGreg Roach * @param Tree $tree 132ce42304aSGreg Roach * @param array<string,string> $params 133ce42304aSGreg Roach * 13436779af1SGreg Roach * @return Collection<int,string>|null 135ce42304aSGreg Roach */ 1361ff45046SGreg Roach protected function familiesToFix(Tree $tree, array $params): Collection|null 137ce42304aSGreg Roach { 138748dbe15SGreg Roach if ($params['type'] !== Family::RECORD_TYPE || $params['search-for'] === '') { 139ce42304aSGreg Roach return null; 140ce42304aSGreg Roach } 141ce42304aSGreg Roach 142ce42304aSGreg Roach $query = DB::table('families')->where('f_file', '=', $tree->id()); 143ce42304aSGreg Roach $this->recordQuery($query, 'f_gedcom', $params); 144ce42304aSGreg Roach 145ce42304aSGreg Roach return $query->pluck('f_id'); 146ce42304aSGreg Roach } 147ce42304aSGreg Roach 148ce42304aSGreg Roach /** 149ce42304aSGreg Roach * A list of all records that need examining. This may include records 150ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 151ce42304aSGreg Roach * 152ce42304aSGreg Roach * @param Tree $tree 153ce42304aSGreg Roach * @param array<string,string> $params 154ce42304aSGreg Roach * 15536779af1SGreg Roach * @return Collection<int,string>|null 156ce42304aSGreg Roach */ 1571ff45046SGreg Roach protected function individualsToFix(Tree $tree, array $params): Collection|null 158ce42304aSGreg Roach { 159748dbe15SGreg Roach if ($params['type'] !== Individual::RECORD_TYPE || $params['search-for'] === '') { 160ce42304aSGreg Roach return null; 161ce42304aSGreg Roach } 162ce42304aSGreg Roach 163ce42304aSGreg Roach $query = DB::table('individuals') 164ce42304aSGreg Roach ->where('i_file', '=', $tree->id()); 165ce42304aSGreg Roach 166ce42304aSGreg Roach $this->recordQuery($query, 'i_gedcom', $params); 167ce42304aSGreg Roach 168ce42304aSGreg Roach return $query->pluck('i_id'); 169ce42304aSGreg Roach } 170ce42304aSGreg Roach 171ce42304aSGreg Roach /** 172ce42304aSGreg Roach * A list of all records that need examining. This may include records 173ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 174ce42304aSGreg Roach * 175ce42304aSGreg Roach * @param Tree $tree 176ce42304aSGreg Roach * @param array<string,string> $params 177ce42304aSGreg Roach * 17836779af1SGreg Roach * @return Collection<int,string>|null 179ce42304aSGreg Roach */ 1801ff45046SGreg Roach protected function locationsToFix(Tree $tree, array $params): Collection|null 18192657c8aSGreg Roach { 182748dbe15SGreg Roach if ($params['type'] !== Location::RECORD_TYPE || $params['search-for'] === '') { 18392657c8aSGreg Roach return null; 18492657c8aSGreg Roach } 18592657c8aSGreg Roach 18692657c8aSGreg Roach $query = DB::table('other') 18792657c8aSGreg Roach ->where('o_file', '=', $tree->id()) 18892657c8aSGreg Roach ->where('o_type', '=', Location::RECORD_TYPE); 18992657c8aSGreg Roach 19092657c8aSGreg Roach $this->recordQuery($query, 'o_gedcom', $params); 19192657c8aSGreg Roach 19292657c8aSGreg Roach return $query->pluck('o_id'); 19392657c8aSGreg Roach } 19492657c8aSGreg Roach 19592657c8aSGreg Roach /** 19692657c8aSGreg Roach * A list of all records that need examining. This may include records 19792657c8aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 19892657c8aSGreg Roach * 19992657c8aSGreg Roach * @param Tree $tree 20092657c8aSGreg Roach * @param array<string,string> $params 20192657c8aSGreg Roach * 20236779af1SGreg Roach * @return Collection<int,string>|null 20392657c8aSGreg Roach */ 2041ff45046SGreg Roach protected function mediaToFix(Tree $tree, array $params): Collection|null 205ce42304aSGreg Roach { 206748dbe15SGreg Roach if ($params['type'] !== Media::RECORD_TYPE || $params['search-for'] === '') { 207ce42304aSGreg Roach return null; 208ce42304aSGreg Roach } 209ce42304aSGreg Roach 210ce42304aSGreg Roach $query = DB::table('media') 211ce42304aSGreg Roach ->where('m_file', '=', $tree->id()); 212ce42304aSGreg Roach 213ce42304aSGreg Roach $this->recordQuery($query, 'm_gedcom', $params); 214ce42304aSGreg Roach 215ce42304aSGreg Roach return $query->pluck('m_id'); 216ce42304aSGreg Roach } 217ce42304aSGreg Roach 218ce42304aSGreg Roach /** 219ce42304aSGreg Roach * A list of all records that need examining. This may include records 220ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 221ce42304aSGreg Roach * 222ce42304aSGreg Roach * @param Tree $tree 223ce42304aSGreg Roach * @param array<string,string> $params 224ce42304aSGreg Roach * 22536779af1SGreg Roach * @return Collection<int,string>|null 226ce42304aSGreg Roach */ 2271ff45046SGreg Roach protected function notesToFix(Tree $tree, array $params): Collection|null 228ce42304aSGreg Roach { 229748dbe15SGreg Roach if ($params['type'] !== Note::RECORD_TYPE || $params['search-for'] === '') { 230ce42304aSGreg Roach return null; 231ce42304aSGreg Roach } 232ce42304aSGreg Roach 233ce42304aSGreg Roach $query = DB::table('other') 234ce42304aSGreg Roach ->where('o_file', '=', $tree->id()) 235ce42304aSGreg Roach ->where('o_type', '=', Note::RECORD_TYPE); 236ce42304aSGreg Roach 237ce42304aSGreg Roach $this->recordQuery($query, 'o_gedcom', $params); 238ce42304aSGreg Roach 239ce42304aSGreg Roach return $query->pluck('o_id'); 240ce42304aSGreg Roach } 241ce42304aSGreg Roach 242ce42304aSGreg Roach /** 243ce42304aSGreg Roach * A list of all records that need examining. This may include records 244ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 245ce42304aSGreg Roach * 246ce42304aSGreg Roach * @param Tree $tree 247ce42304aSGreg Roach * @param array<string,string> $params 248ce42304aSGreg Roach * 24936779af1SGreg Roach * @return Collection<int,string>|null 250ce42304aSGreg Roach */ 2511ff45046SGreg Roach protected function repositoriesToFix(Tree $tree, array $params): Collection|null 252ce42304aSGreg Roach { 253748dbe15SGreg Roach if ($params['type'] !== Repository::RECORD_TYPE || $params['search-for'] === '') { 254ce42304aSGreg Roach return null; 255ce42304aSGreg Roach } 256ce42304aSGreg Roach 257ce42304aSGreg Roach $query = DB::table('other') 258ce42304aSGreg Roach ->where('o_file', '=', $tree->id()) 259ce42304aSGreg Roach ->where('o_type', '=', Repository::RECORD_TYPE); 260ce42304aSGreg Roach 261ce42304aSGreg Roach $this->recordQuery($query, 'o_gedcom', $params); 262ce42304aSGreg Roach 263ce42304aSGreg Roach return $query->pluck('o_id'); 264ce42304aSGreg Roach } 265ce42304aSGreg Roach 266ce42304aSGreg Roach /** 267ce42304aSGreg Roach * A list of all records that need examining. This may include records 268ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 269ce42304aSGreg Roach * 270ce42304aSGreg Roach * @param Tree $tree 271ce42304aSGreg Roach * @param array<string,string> $params 272ce42304aSGreg Roach * 27336779af1SGreg Roach * @return Collection<int,string>|null 274ce42304aSGreg Roach */ 2751ff45046SGreg Roach protected function sourcesToFix(Tree $tree, array $params): Collection|null 276ce42304aSGreg Roach { 277748dbe15SGreg Roach if ($params['type'] !== Source::RECORD_TYPE || $params['search-for'] === '') { 278ce42304aSGreg Roach return null; 279ce42304aSGreg Roach } 280ce42304aSGreg Roach 2817684867eSGreg Roach $query = $this->sourcesToFixQuery($tree, $params); 282ce42304aSGreg Roach 283ce42304aSGreg Roach $this->recordQuery($query, 's_gedcom', $params); 284ce42304aSGreg Roach 285ce42304aSGreg Roach return $query->pluck('s_id'); 286ce42304aSGreg Roach } 287ce42304aSGreg Roach 288ce42304aSGreg Roach /** 289ce42304aSGreg Roach * A list of all records that need examining. This may include records 290ce42304aSGreg Roach * that do not need updating, if we can't detect this quickly using SQL. 291ce42304aSGreg Roach * 292ce42304aSGreg Roach * @param Tree $tree 293ce42304aSGreg Roach * @param array<string,string> $params 294ce42304aSGreg Roach * 29536779af1SGreg Roach * @return Collection<int,string>|null 296ce42304aSGreg Roach */ 2971ff45046SGreg Roach protected function submittersToFix(Tree $tree, array $params): Collection|null 298ce42304aSGreg Roach { 299748dbe15SGreg Roach if ($params['type'] !== Submitter::RECORD_TYPE || $params['search-for'] === '') { 300ce42304aSGreg Roach return null; 301ce42304aSGreg Roach } 302ce42304aSGreg Roach 3037684867eSGreg Roach $query = $this->submittersToFixQuery($tree, $params); 304ce42304aSGreg Roach 305ce42304aSGreg Roach $this->recordQuery($query, 'o_gedcom', $params); 306ce42304aSGreg Roach 307ce42304aSGreg Roach return $query->pluck('o_id'); 308ce42304aSGreg Roach } 309ce42304aSGreg Roach 310ce42304aSGreg Roach /** 311ce42304aSGreg Roach * Does a record need updating? 312ce42304aSGreg Roach * 313ce42304aSGreg Roach * @param GedcomRecord $record 314ce42304aSGreg Roach * @param array<string,string> $params 315ce42304aSGreg Roach * 316ce42304aSGreg Roach * @return bool 317ce42304aSGreg Roach */ 318ce42304aSGreg Roach public function doesRecordNeedUpdate(GedcomRecord $record, array $params): bool 319ce42304aSGreg Roach { 320ce42304aSGreg Roach return preg_match($this->createRegex($params), $record->gedcom()) === 1; 321ce42304aSGreg Roach } 322ce42304aSGreg Roach 323ce42304aSGreg Roach /** 324ce42304aSGreg Roach * Show the changes we would make 325ce42304aSGreg Roach * 326ce42304aSGreg Roach * @param GedcomRecord $record 327ce42304aSGreg Roach * @param array<string,string> $params 328ce42304aSGreg Roach * 329ce42304aSGreg Roach * @return string 330ce42304aSGreg Roach */ 331ce42304aSGreg Roach public function previewUpdate(GedcomRecord $record, array $params): string 332ce42304aSGreg Roach { 333ce42304aSGreg Roach $old = $record->gedcom(); 334ce42304aSGreg Roach $new = $this->updateGedcom($record, $params); 335ce42304aSGreg Roach 336ce42304aSGreg Roach return $this->data_fix_service->gedcomDiff($record->tree(), $old, $new); 337ce42304aSGreg Roach } 338ce42304aSGreg Roach 339ce42304aSGreg Roach /** 340ce42304aSGreg Roach * Fix a record 341ce42304aSGreg Roach * 342ce42304aSGreg Roach * @param GedcomRecord $record 343ce42304aSGreg Roach * @param array<string,string> $params 344ce42304aSGreg Roach * 345ce42304aSGreg Roach * @return void 346ce42304aSGreg Roach */ 347ce42304aSGreg Roach public function updateRecord(GedcomRecord $record, array $params): void 348ce42304aSGreg Roach { 349ce42304aSGreg Roach $record->updateRecord($this->updateGedcom($record, $params), false); 350ce42304aSGreg Roach } 351ce42304aSGreg Roach 352ce42304aSGreg Roach /** 353ce42304aSGreg Roach * @param GedcomRecord $record 354ce42304aSGreg Roach * @param array<string,string> $params 355ce42304aSGreg Roach * 356ce42304aSGreg Roach * @return string 357ce42304aSGreg Roach */ 358ce42304aSGreg Roach private function updateGedcom(GedcomRecord $record, array $params): string 359ce42304aSGreg Roach { 360ce42304aSGreg Roach // Allow "\n" to indicate a line-feed in replacement text. 361ce42304aSGreg Roach // Back-references such as $1, $2 are handled automatically. 362748dbe15SGreg Roach $replace = strtr($params['replace-with'], ['\n' => "\n"]); 363ce42304aSGreg Roach 364ce42304aSGreg Roach $regex = $this->createRegex($params); 365ce42304aSGreg Roach 366ce42304aSGreg Roach return preg_replace($regex, $replace, $record->gedcom()); 367ce42304aSGreg Roach } 368ce42304aSGreg Roach 369ce42304aSGreg Roach /** 370ce42304aSGreg Roach * Create a regular expression from the search pattern. 371ce42304aSGreg Roach * 372ce42304aSGreg Roach * @param array<string,string> $params 373ce42304aSGreg Roach * 374ce42304aSGreg Roach * @return string 375ce42304aSGreg Roach */ 376ce42304aSGreg Roach private function createRegex(array $params): string 377ce42304aSGreg Roach { 378748dbe15SGreg Roach $search = $params['search-for']; 379ce42304aSGreg Roach $method = $params['method']; 380ce42304aSGreg Roach $case = $params['case']; 381ce42304aSGreg Roach 382ce42304aSGreg Roach switch ($method) { 383ce42304aSGreg Roach case 'exact': 384681f0c68SGreg Roach return '/' . preg_quote($search, '/') . '/u' . $case; 385ce42304aSGreg Roach 386ce42304aSGreg Roach case 'words': 387681f0c68SGreg Roach return '/\b' . preg_quote($search, '/') . '\b/u' . $case; 388ce42304aSGreg Roach 389ce42304aSGreg Roach case 'wildcards': 390681f0c68SGreg Roach return '/\b' . strtr(preg_quote($search, '/'), ['\*' => '.*', '\?' => '.']) . '\b/u' . $case; 391ce42304aSGreg Roach 392ce42304aSGreg Roach case 'regex': 393a4f494bbSGreg Roach $regex = '/' . addcslashes($search, '/') . '/u' . $case; 394ce42304aSGreg Roach 395ce42304aSGreg Roach try { 396ce42304aSGreg Roach // A valid regex on an empty string returns zero. 397ce42304aSGreg Roach // An invalid regex on an empty string returns false and throws a warning. 398ce42304aSGreg Roach preg_match($regex, ''); 39928d026adSGreg Roach } catch (Throwable) { 400ce42304aSGreg Roach $regex = self::INVALID_REGEX; 401ce42304aSGreg Roach } 402ce42304aSGreg Roach 403ce42304aSGreg Roach return $regex; 404ce42304aSGreg Roach } 405ce42304aSGreg Roach 406ce42304aSGreg Roach throw new HttpNotFoundException(); 407ce42304aSGreg Roach } 408ce42304aSGreg Roach 409ce42304aSGreg Roach /** 410ce42304aSGreg Roach * Create a regular expression from the search pattern. 411ce42304aSGreg Roach * 412ce42304aSGreg Roach * @param Builder $query 413ce42304aSGreg Roach * @param string $column 414ce42304aSGreg Roach * @param array<string,string> $params 415ce42304aSGreg Roach * 416ce42304aSGreg Roach * @return void 417ce42304aSGreg Roach */ 418ce42304aSGreg Roach private function recordQuery(Builder $query, string $column, array $params): void 419ce42304aSGreg Roach { 420748dbe15SGreg Roach $search = $params['search-for']; 421ce42304aSGreg Roach $method = $params['method']; 422b5961194SGreg Roach $like = '%' . addcslashes($search, '\\%_') . '%'; 423ce42304aSGreg Roach 424ce42304aSGreg Roach switch ($method) { 425ce42304aSGreg Roach case 'exact': 426ce42304aSGreg Roach case 'words': 427b5961194SGreg Roach $query->where($column, 'LIKE', $like); 428ce42304aSGreg Roach break; 429ce42304aSGreg Roach 430ce42304aSGreg Roach case 'wildcards': 431ce42304aSGreg Roach $like = strtr($like, ['?' => '_', '*' => '%']); 432b5961194SGreg Roach $query->where($column, 'LIKE', $like); 433ce42304aSGreg Roach break; 434ce42304aSGreg Roach 435ce42304aSGreg Roach case 'regex': 4369a92a0c1SGreg Roach // Substituting newlines seems to be necessary on *some* versions 437e93aa0bdSGreg Roach // of MySQL (e.g. 5.7), and harmless on others (e.g. 8.0). 4389a92a0c1SGreg Roach $search = strtr($search, ['\n' => "\n"]); 4399a92a0c1SGreg Roach 440*52550490SGreg Roach $query->where($column, DB::regexOperator(), $search); 441ce42304aSGreg Roach break; 442ce42304aSGreg Roach } 443ce42304aSGreg Roach } 444ce42304aSGreg Roach} 445