Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use OCA\DAV\Command\RemoveInvalidShares;
use OCA\DAV\Connector\Sabre\Principal;
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Server;
Expand Down Expand Up @@ -39,18 +40,25 @@ protected function setUp(): void {
$this->principalBackend = $this->createMock(Principal::class);
$this->remoteUserPrincipalBackend = $this->createMock(RemoteUserPrincipalBackend::class);

$this->db->insertIfNotExist('*PREFIX*dav_shares', [
'principaluri' => 'principal:unknown',
'type' => 'calendar',
'access' => 2,
'resourceid' => 666,
]);
$this->db->insertIfNotExist('*PREFIX*dav_shares', [
'principaluri' => 'principals/remote-users/foobar',
'type' => 'calendar',
'access' => 2,
'resourceid' => 666,
]);
$qb = $this->db->getQueryBuilder();
$qb->insert('dav_shares');

foreach (['principal:unknown', 'principals/remote-users/foobar'] as $principaluri) {
$qb->values([
'principaluri' => $qb->createNamedParameter($principaluri),
'type' => $qb->createNamedParameter('calendar'),
'access' => $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT),
'resourceid' => $qb->createNamedParameter(666, IQueryBuilder::PARAM_INT),
]);

try {
$qb->executeStatement();
} catch (Exception $e) {
if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
throw $e;
}
}
}

$this->command = new RemoveInvalidShares(
$this->db,
Expand Down
50 changes: 33 additions & 17 deletions apps/files_sharing/lib/External/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,27 +88,43 @@ public function addShare($remote, $token, $password, $name, $owner, $shareType,
// using the original share item name.
$tmpMountPointName = '{{TemporaryMountPointName#' . $name . '}}';
$mountPoint = $tmpMountPointName;
$hash = md5($tmpMountPointName);
$data = [
'remote' => $remote,
'share_token' => $token,
'password' => $password,
'name' => $name,
'owner' => $owner,
'user' => $user,
'mountpoint' => $mountPoint,
'mountpoint_hash' => $hash,
'accepted' => $accepted,
'remote_id' => $remoteId,
'share_type' => $shareType,

$qb = $this->connection->getQueryBuilder();
$qb->insert('share_external');

$values = [
'remote' => $qb->createNamedParameter($remote),
'share_token' => $qb->createNamedParameter($token),
'password' => $qb->createNamedParameter($password),
'name' => $qb->createNamedParameter($name),
'owner' => $qb->createNamedParameter($owner),
'user' => $qb->createNamedParameter($user),
'accepted' => $qb->createNamedParameter($accepted, IQueryBuilder::PARAM_INT),
'remote_id' => $qb->createNamedParameter($remoteId),
'share_type' => $qb->createNamedParameter($shareType, IQueryBuilder::PARAM_INT),
];

$i = 1;
while (!$this->connection->insertIfNotExist('*PREFIX*share_external', $data, ['user', 'mountpoint_hash'])) {
while (true) {
// The external share already exists for the user
$data['mountpoint'] = $tmpMountPointName . '-' . $i;
$data['mountpoint_hash'] = md5($data['mountpoint']);
$i++;
$qb->values(array_merge($values, [
'mountpoint' => $qb->createNamedParameter($mountPoint),
'mountpoint_hash' => $qb->createNamedParameter(md5($mountPoint)),
]));

try {
$qb->executeStatement();

break;
} catch (Exception $e) {
if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
$mountPoint = $tmpMountPointName . '-' . $i;
$i++;
continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No risk of infinite loop?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but this was already the case before. The sharding tests are running into a timeout on some tests, so that could be related (but I don't see why yet).

}

throw $e;
}
}
return null;
}
Expand Down
34 changes: 20 additions & 14 deletions apps/user_ldap/lib/Mapping/AbstractMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -416,23 +416,29 @@ public function map($fdn, $name, $uuid) {
return false;
}

$row = [
'ldap_dn_hash' => $this->getDNHash($fdn),
'ldap_dn' => $fdn,
'owncloud_name' => $name,
'directory_uuid' => $uuid
];
$qb = $this->dbc->getQueryBuilder();
$qb
->insert($this->getTableName())
->values([
'ldap_dn_hash' => $qb->createNamedParameter($this->getDNHash($fdn)),
'ldap_dn' => $qb->createNamedParameter($fdn),
'owncloud_name' => $qb->createNamedParameter($name),
'directory_uuid' => $qb->createNamedParameter($uuid),
]);

try {
$result = $this->dbc->insertIfNotExist($this->getTableName(), $row);
if ((bool)$result === true) {
$this->cache[$fdn] = $name;
$this->localNameToDnCache?->set($name, $fdn, self::LOCAL_CACHE_TTL);
$qb->executeStatement();

$this->cache[$fdn] = $name;
$this->localNameToDnCache?->set($name, $fdn, self::LOCAL_CACHE_TTL);

return true;
} catch (\OCP\DB\Exception $e) {
if ($e->getReason() === \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
return false;
}
// insertIfNotExist returns values as int
return (bool)$result;
} catch (\Exception $e) {
return false;

throw $e;
}
}

Expand Down
2 changes: 0 additions & 2 deletions build/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1691,7 +1691,6 @@
<DeprecatedMethod>
<code><![CDATA[Files::buildNotExistingFileName($shareFolder, $share['name'])]]></code>
<code><![CDATA[Files::buildNotExistingFileName('/', $name)]]></code>
<code><![CDATA[insertIfNotExist]]></code>
<code><![CDATA[sendNotification]]></code>
<code><![CDATA[sendNotification]]></code>
</DeprecatedMethod>
Expand Down Expand Up @@ -2921,7 +2920,6 @@
<file src="apps/user_ldap/lib/Mapping/AbstractMapping.php">
<DeprecatedMethod>
<code><![CDATA[getDatabasePlatform]]></code>
<code><![CDATA[insertIfNotExist]]></code>
</DeprecatedMethod>
<RedundantCondition>
<code><![CDATA[isset($qb)]]></code>
Expand Down
49 changes: 0 additions & 49 deletions lib/private/DB/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
namespace OC\DB;

use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use OC\DB\Exceptions\DbalException;

/**
Expand Down Expand Up @@ -64,54 +63,6 @@ public function unlockTable() {
$this->conn->commit();
}

/**
* Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
* it is needed that there is also a unique constraint on the values. Then this method will
* catch the exception and return 0.
*
* @param string $table The table name (will replace *PREFIX* with the actual prefix)
* @param array $input data that should be inserted into the table (column name => value)
* @param array|null $compare List of values that should be checked for "if not exists"
* If this is null or an empty array, all keys of $input will be compared
* Please note: text fields (clob) must not be used in the compare array
* @return int number of inserted rows
* @throws Exception
* @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
*/
public function insertIfNotExist($table, $input, ?array $compare = null) {
$compare = $compare ?: array_keys($input);

// Prepare column names and generate placeholders
$columns = '`' . implode('`,`', array_keys($input)) . '`';
$placeholders = implode(', ', array_fill(0, count($input), '?'));

$query = 'INSERT INTO `' . $table . '` (' . $columns . ') '
. 'SELECT ' . $placeholders . ' '
. 'FROM `' . $table . '` WHERE ';

$inserts = array_values($input);
foreach ($compare as $key) {
$query .= '`' . $key . '`';
if (is_null($input[$key])) {
$query .= ' IS NULL AND ';
} else {
$inserts[] = $input[$key];
$query .= ' = ? AND ';
}
}
$query = substr($query, 0, -5);
$query .= ' HAVING COUNT(*) = 0';

try {
return $this->conn->executeUpdate($query, $inserts);
} catch (UniqueConstraintViolationException $e) {
// This exception indicates a concurrent insert happened between
// the insert and the sub-select in the insert, which is safe to ignore.
// More details: https://github.com/nextcloud/server/pull/12315
return 0;
}
}

/**
* @throws \OCP\DB\Exception
*/
Expand Down
49 changes: 0 additions & 49 deletions lib/private/DB/AdapterSqlite.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
*/
namespace OC\DB;

use Doctrine\DBAL\Exception\UniqueConstraintViolationException;

class AdapterSqlite extends Adapter {
/**
* @param string $tableName
Expand All @@ -30,53 +28,6 @@ public function fixupStatement($statement) {
return $statement;
}

/**
* Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
* it is needed that there is also a unique constraint on the values. Then this method will
* catch the exception and return 0.
*
* @param string $table The table name (will replace *PREFIX* with the actual prefix)
* @param array $input data that should be inserted into the table (column name => value)
* @param array|null $compare List of values that should be checked for "if not exists"
* If this is null or an empty array, all keys of $input will be compared
* Please note: text fields (clob) must not be used in the compare array
* @return int number of inserted rows
* @throws \Doctrine\DBAL\Exception
* @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
*/
public function insertIfNotExist($table, $input, ?array $compare = null) {
if (empty($compare)) {
$compare = array_keys($input);
}
$fieldList = '`' . implode('`,`', array_keys($input)) . '`';
$query = "INSERT INTO `$table` ($fieldList) SELECT "
. str_repeat('?,', count($input) - 1) . '? '
. " WHERE NOT EXISTS (SELECT 1 FROM `$table` WHERE ";

$inserts = array_values($input);
foreach ($compare as $key) {
$query .= '`' . $key . '`';
if (is_null($input[$key])) {
$query .= ' IS NULL AND ';
} else {
$inserts[] = $input[$key];
$query .= ' = ? AND ';
}
}
$query = substr($query, 0, -5);
$query .= ')';

try {
return $this->conn->executeUpdate($query, $inserts);
} catch (UniqueConstraintViolationException $e) {
// if this is thrown then a concurrent insert happened between the insert and the sub-select in the insert, that should have avoided it
// it's fine to ignore this then
//
// more discussions about this can be found at https://github.com/nextcloud/server/pull/12315
return 0;
}
}

public function insertIgnoreConflict(string $table, array $values): int {
$builder = $this->conn->getQueryBuilder();
$builder->insert($table);
Expand Down
23 changes: 0 additions & 23 deletions lib/private/DB/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -531,29 +531,6 @@ public function realLastInsertId($seqName = null) {
return parent::lastInsertId($seqName);
}

/**
* Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
* it is needed that there is also a unique constraint on the values. Then this method will
* catch the exception and return 0.
*
* @param string $table The table name (will replace *PREFIX* with the actual prefix)
* @param array $input data that should be inserted into the table (column name => value)
* @param array|null $compare List of values that should be checked for "if not exists"
* If this is null or an empty array, all keys of $input will be compared
* Please note: text fields (clob) must not be used in the compare array
* @return int number of inserted rows
* @throws \Doctrine\DBAL\Exception
* @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
*/
public function insertIfNotExist($table, $input, ?array $compare = null) {
try {
return $this->adapter->insertIfNotExist($table, $input, $compare);
} catch (\Exception $e) {
$this->logDatabaseException($e);
throw $e;
}
}

public function insertIgnoreConflict(string $table, array $values) : int {
try {
return $this->adapter->insertIgnoreConflict($table, $values);
Expand Down
8 changes: 0 additions & 8 deletions lib/private/DB/ConnectionAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,6 @@ public function lastInsertId(string $table): int {
}
}

public function insertIfNotExist(string $table, array $input, ?array $compare = null) {
try {
return $this->inner->insertIfNotExist($table, $input, $compare);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}

public function insertIgnoreConflict(string $table, array $values): int {
try {
return $this->inner->insertIgnoreConflict($table, $values);
Expand Down
19 changes: 15 additions & 4 deletions lib/private/DB/MigrationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use OCP\App\IAppManager;
use OCP\AppFramework\App;
use OCP\AppFramework\QueryException;
use OCP\DB\Exception;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\IDBConnection;
Expand Down Expand Up @@ -283,10 +284,20 @@ private function shallBeExecuted($m, $knownMigrations) {
* @param string $version
*/
private function markAsExecuted($version) {
$this->connection->insertIfNotExist('*PREFIX*migrations', [
'app' => $this->appName,
'version' => $version
]);
$qb = $this->connection->getQueryBuilder();
$qb
->insert('migrations')
->values([
'app' => $qb->createNamedParameter($this->appName),
'version' => $qb->createNamedParameter($version),
]);
try {
$qb->executeStatement();
} catch (Exception $e) {
if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
throw $e;
}
}
}

/**
Expand Down
Loading
Loading