-
Notifications
You must be signed in to change notification settings - Fork 4
feat: command to update source links of molecules of any collection. #692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a new console command coconut:update-links that allows bulk updating of source links for molecules in a specific collection. The command updates URLs in both the entries and collection_molecule tables by replacing an old link prefix with a new one.
Key Changes
- New Artisan command for updating molecule source links across two database tables
- Uses SQL
REPLACE()function to perform bulk string replacement on link/URL columns - Accepts three arguments: collection ID, old link prefix, and new link prefix
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.
app/Console/Commands/UpdateLinks.php
Outdated
| $entriesUpdated = DB::table('entries') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('link', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | ||
| ]); | ||
|
|
||
| // Update collection_molecule table | ||
| $moleculesUpdated = DB::table('collection_molecule') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('url', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | ||
| ]); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical SQL Injection Vulnerability: The command arguments are directly interpolated into raw SQL strings without proper parameter binding, creating multiple SQL injection vulnerabilities:
- Line 31:
'like', $oldPrefix.'%'- direct concatenation in LIKE clause - Line 33:
DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')")- direct interpolation in DB::raw()
Recommendation: Use DB::update() with parameter binding instead of the query builder's update method with DB::raw():
$sql = "UPDATE collection_molecule SET url = REPLACE(url, ?, ?) WHERE collection_id = ? AND url LIKE ?";
$moleculesUpdated = DB::update($sql, [$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%']);This approach is used elsewhere in the codebase (see app/Console/Commands/UpdateNamesFromSynonyms.php:65).
| $entriesUpdated = DB::table('entries') | |
| ->where('collection_id', $collectionId) | |
| ->where('link', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| // Update collection_molecule table | |
| $moleculesUpdated = DB::table('collection_molecule') | |
| ->where('collection_id', $collectionId) | |
| ->where('url', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| $sqlEntries = "UPDATE entries SET link = REPLACE(link, ?, ?) WHERE collection_id = ? AND link LIKE ?"; | |
| $entriesUpdated = DB::update($sqlEntries, [$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%']); | |
| // Update collection_molecule table | |
| $sqlMolecules = "UPDATE collection_molecule SET url = REPLACE(url, ?, ?) WHERE collection_id = ? AND url LIKE ?"; | |
| $moleculesUpdated = DB::update($sqlMolecules, [$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%']); |
app/Console/Commands/UpdateLinks.php
Outdated
| // Update entries table | ||
| $entriesUpdated = DB::table('entries') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('link', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | ||
| ]); | ||
|
|
||
| // Update collection_molecule table | ||
| $moleculesUpdated = DB::table('collection_molecule') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('url', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | ||
| ]); | ||
|
|
||
| $this->info("Updated $entriesUpdated entries and $moleculesUpdated collection_molecule records."); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The database updates are not wrapped in a transaction. If the entries table update succeeds but the collection_molecule table update fails (or vice versa), the database will be left in an inconsistent state.
Recommendation: Wrap the database operations in a DB::transaction() to ensure atomicity, consistent with other console commands in the codebase:
public function handle()
{
$collectionId = $this->argument('collection_id');
$oldPrefix = $this->argument('old_link');
$newPrefix = $this->argument('new_link');
DB::transaction(function () use ($collectionId, $oldPrefix, $newPrefix) {
// Update entries table
$entriesUpdated = DB::table('entries')
// ... rest of the code
// Update collection_molecule table
$moleculesUpdated = DB::table('collection_molecule')
// ... rest of the code
$this->info("Updated $entriesUpdated entries and $moleculesUpdated collection_molecule records.");
});
}| // Update entries table | |
| $entriesUpdated = DB::table('entries') | |
| ->where('collection_id', $collectionId) | |
| ->where('link', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| // Update collection_molecule table | |
| $moleculesUpdated = DB::table('collection_molecule') | |
| ->where('collection_id', $collectionId) | |
| ->where('url', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| $this->info("Updated $entriesUpdated entries and $moleculesUpdated collection_molecule records."); | |
| DB::transaction(function () use ($collectionId, $oldPrefix, $newPrefix) { | |
| // Update entries table | |
| $entriesUpdated = DB::table('entries') | |
| ->where('collection_id', $collectionId) | |
| ->where('link', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| // Update collection_molecule table | |
| $moleculesUpdated = DB::table('collection_molecule') | |
| ->where('collection_id', $collectionId) | |
| ->where('url', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| $this->info("Updated $entriesUpdated entries and $moleculesUpdated collection_molecule records."); | |
| }); |
app/Console/Commands/UpdateLinks.php
Outdated
| $entriesUpdated = DB::table('entries') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('link', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | ||
| ]); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The SQL REPLACE() function replaces all occurrences of the search string in the column, not just the first occurrence or prefix. If a URL contains the old link multiple times (e.g., in query parameters or fragments), all occurrences will be replaced.
While the LIKE filter ensures only URLs starting with the old prefix are updated, the replacement itself is global within each matching URL. Consider if this is the intended behavior.
Example: If old_link is http://old.com and a URL is http://old.com/page?ref=http://old.com, both occurrences will be replaced.
If you only want to replace the prefix, consider using SQL's CONCAT() function instead:
$sql = "UPDATE entries SET link = CONCAT(?, SUBSTR(link, LENGTH(?) + 1)) WHERE collection_id = ? AND link LIKE ?";
DB::update($sql, [$newPrefix, $oldPrefix, $collectionId, $oldPrefix.'%']);
app/Console/Commands/UpdateLinks.php
Outdated
| $entriesUpdated = DB::table('entries') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('link', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | ||
| ]); | ||
|
|
||
| // Update collection_molecule table | ||
| $moleculesUpdated = DB::table('collection_molecule') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('url', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | ||
| ]); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical SQL Injection Vulnerability: The command arguments are directly interpolated into raw SQL strings without proper parameter binding, creating multiple SQL injection vulnerabilities:
- Line 23:
'like', $oldPrefix.'%'- direct concatenation in LIKE clause - Line 25:
DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')")- direct interpolation in DB::raw()
Recommendation: Use DB::update() with parameter binding instead of the query builder's update method with DB::raw():
$sql = "UPDATE entries SET link = REPLACE(link, ?, ?) WHERE collection_id = ? AND link LIKE ?";
$entriesUpdated = DB::update($sql, [$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%']);This approach is used elsewhere in the codebase (see app/Console/Commands/UpdateNamesFromSynonyms.php:65).
| $entriesUpdated = DB::table('entries') | |
| ->where('collection_id', $collectionId) | |
| ->where('link', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| // Update collection_molecule table | |
| $moleculesUpdated = DB::table('collection_molecule') | |
| ->where('collection_id', $collectionId) | |
| ->where('url', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| $sqlEntries = "UPDATE entries SET link = REPLACE(link, ?, ?) WHERE collection_id = ? AND link LIKE ?"; | |
| $entriesUpdated = DB::update($sqlEntries, [$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%']); | |
| // Update collection_molecule table | |
| $sqlMolecules = "UPDATE collection_molecule SET url = REPLACE(url, ?, ?) WHERE collection_id = ? AND url LIKE ?"; | |
| $moleculesUpdated = DB::update($sqlMolecules, [$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.
app/Console/Commands/UpdateLinks.php
Outdated
| $entriesUpdated = DB::table('entries') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('link', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | ||
| ]); | ||
|
|
||
| // Update collection_molecule table | ||
| $moleculesUpdated = DB::table('collection_molecule') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('url', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | ||
| ]); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SQL injection vulnerability: User input is directly interpolated into the SQL REPLACE function without proper parameter binding. An attacker could provide malicious input in old_link or new_link arguments to execute arbitrary SQL.
Use parameter binding instead:
'link' => DB::raw("REPLACE(link, ?, ?)"),And pass the parameters as the second argument to the update method:
->update([
'link' => DB::raw("REPLACE(link, ?, ?)"),
], [$oldPrefix, $newPrefix])However, Laravel's query builder doesn't support binding parameters in DB::raw() within update arrays. A safer approach is to use DB::update() with parameter binding:
$entriesUpdated = DB::update(
"UPDATE entries SET link = REPLACE(link, ?, ?) WHERE collection_id = ? AND link LIKE ?",
[$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%']
);| $entriesUpdated = DB::table('entries') | |
| ->where('collection_id', $collectionId) | |
| ->where('link', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| // Update collection_molecule table | |
| $moleculesUpdated = DB::table('collection_molecule') | |
| ->where('collection_id', $collectionId) | |
| ->where('url', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| // Update entries table with parameter binding | |
| $entriesUpdated = DB::update( | |
| "UPDATE entries SET link = REPLACE(link, ?, ?) WHERE collection_id = ? AND link LIKE ?", | |
| [$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%'] | |
| ); | |
| // Update collection_molecule table with parameter binding | |
| $moleculesUpdated = DB::update( | |
| "UPDATE collection_molecule SET url = REPLACE(url, ?, ?) WHERE collection_id = ? AND url LIKE ?", | |
| [$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%'] | |
| ); |
app/Console/Commands/UpdateLinks.php
Outdated
| $entriesUpdated = DB::table('entries') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('link', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | ||
| ]); | ||
|
|
||
| // Update collection_molecule table | ||
| $moleculesUpdated = DB::table('collection_molecule') | ||
| ->where('collection_id', $collectionId) | ||
| ->where('url', 'like', $oldPrefix.'%') | ||
| ->update([ | ||
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | ||
| ]); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SQL injection vulnerability: User input is directly interpolated into the SQL REPLACE function without proper parameter binding. An attacker could provide malicious input in old_link or new_link arguments to execute arbitrary SQL.
Use DB::update() with parameter binding instead:
$moleculesUpdated = DB::update(
"UPDATE collection_molecule SET url = REPLACE(url, ?, ?) WHERE collection_id = ? AND url LIKE ?",
[$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%']
);| $entriesUpdated = DB::table('entries') | |
| ->where('collection_id', $collectionId) | |
| ->where('link', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'link' => DB::raw("REPLACE(link, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| // Update collection_molecule table | |
| $moleculesUpdated = DB::table('collection_molecule') | |
| ->where('collection_id', $collectionId) | |
| ->where('url', 'like', $oldPrefix.'%') | |
| ->update([ | |
| 'url' => DB::raw("REPLACE(url, '$oldPrefix', '$newPrefix')"), | |
| ]); | |
| $entriesUpdated = DB::update( | |
| "UPDATE entries SET link = REPLACE(link, ?, ?) WHERE collection_id = ? AND link LIKE ?", | |
| [$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%'] | |
| ); | |
| // Update collection_molecule table | |
| $moleculesUpdated = DB::update( | |
| "UPDATE collection_molecule SET url = REPLACE(url, ?, ?) WHERE collection_id = ? AND url LIKE ?", | |
| [$oldPrefix, $newPrefix, $collectionId, $oldPrefix.'%'] | |
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.
No description provided.