aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/comments/js/commentstabview.js2
-rw-r--r--apps/theming/lib/Controller/ThemingController.php24
-rw-r--r--apps/theming/tests/Controller/ThemingControllerTest.php88
-rw-r--r--core/Controller/LoginController.php22
-rw-r--r--lib/private/Repair/RepairUnmergedShares.php50
-rw-r--r--tests/Core/Controller/LoginControllerTest.php76
-rw-r--r--tests/lib/Repair/RepairUnmergedSharesTest.php211
7 files changed, 387 insertions, 86 deletions
diff --git a/apps/comments/js/commentstabview.js b/apps/comments/js/commentstabview.js
index 9451e828f91..eae18c1d485 100644
--- a/apps/comments/js/commentstabview.js
+++ b/apps/comments/js/commentstabview.js
@@ -32,7 +32,7 @@
'{{/if}}' +
' </div>' +
' <form class="newCommentForm">' +
- ' <input type="text" class="message" placeholder="{{newMessagePlaceholder}}" value="{{{message}}}" />' +
+ ' <input type="text" class="message" placeholder="{{newMessagePlaceholder}}" value="{{message}}" />' +
' <input class="submit icon-confirm" type="submit" value="" />' +
'{{#if isEditMode}}' +
' <input class="cancel pull-right" type="button" value="{{cancelText}}" />' +
diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php
index fbb4c904773..b4e3a95710f 100644
--- a/apps/theming/lib/Controller/ThemingController.php
+++ b/apps/theming/lib/Controller/ThemingController.php
@@ -304,6 +304,13 @@ class ThemingController extends Controller {
$responseCss = '';
$color = $this->config->getAppValue($this->appName, 'color');
$elementColor = $this->util->elementColor($color);
+
+ if($this->util->invertTextColor($color)) {
+ $textColor = '#000000';
+ } else {
+ $textColor = '#ffffff';
+ }
+
if($color !== '') {
$responseCss .= sprintf(
'#body-user #header,#body-settings #header,#body-public #header,#body-login,.searchbox input[type="search"]:focus,.searchbox input[type="search"]:active,.searchbox input[type="search"]:valid {background-color: %s}' . "\n",
@@ -321,19 +328,26 @@ class ThemingController extends Controller {
'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton($elementColor).'\');' .
"}\n";
$responseCss .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' .
- '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' .
- '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
- '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
- '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' .
'border: 1px solid '.$elementColor.';'.
'background-color: '.$elementColor.';'.
- 'opacity: 0.8' .
+ 'opacity: 0.8;' .
+ 'color: ' . $textColor . ';'.
"}\n" .
'.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' .
'.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' .
'border: 1px solid '.$elementColor.';'.
'background-color: '.$elementColor.';'.
'opacity: 1.0;' .
+ 'color: ' . $textColor . ';'.
+ "}\n" .
+ '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
+ '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
+ '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
+ 'opacity: 0.4;' .
+ 'color: '.$textColor.';'.
"}\n";
$responseCss .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n";
$responseCss .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' .
diff --git a/apps/theming/tests/Controller/ThemingControllerTest.php b/apps/theming/tests/Controller/ThemingControllerTest.php
index d053d8c1a1c..193e0bdcb4b 100644
--- a/apps/theming/tests/Controller/ThemingControllerTest.php
+++ b/apps/theming/tests/Controller/ThemingControllerTest.php
@@ -437,19 +437,26 @@ class ThemingControllerTest extends TestCase {
'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton($color).'\');' .
"}\n";
$expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' .
- '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' .
- '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
- '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
- '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
- 'border: 1px solid '.$color .';'.
+ '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' .
+ 'border: 1px solid '.$color.';'.
'background-color: '.$color.';'.
- 'opacity: 0.8' .
+ 'opacity: 0.8;' .
+ 'color: #ffffff;'.
"}\n" .
'.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' .
'.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' .
'border: 1px solid '.$color.';'.
'background-color: '.$color.';'.
'opacity: 1.0;' .
+ 'color: #ffffff;'.
+ "}\n" .
+ '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
+ '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
+ '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ 'border: 1px solid '.$color.';'.
+ 'background-color: '.$color.';'.
+ 'opacity: 0.4;' .
+ 'color: #ffffff;'.
"}\n";
$expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n";
$expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' .
@@ -520,19 +527,26 @@ class ThemingControllerTest extends TestCase {
'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton('#555555').'\');' .
"}\n";
$expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' .
- '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' .
- '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
- '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
- '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
- 'border: 1px solid #555555;'.
- 'background-color: #555555;'.
- 'opacity: 0.8' .
+ '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' .
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
+ 'opacity: 0.8;' .
+ 'color: #000000;'.
"}\n" .
'.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' .
'.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' .
- 'border: 1px solid #555555;'.
- 'background-color: #555555;'.
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
'opacity: 1.0;' .
+ 'color: #000000;'.
+ "}\n" .
+ '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
+ '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
+ '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
+ 'opacity: 0.4;' .
+ 'color: #000000;'.
"}\n";
$expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n";
$expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' .
@@ -689,19 +703,26 @@ class ThemingControllerTest extends TestCase {
'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton($color).'\');' .
"}\n";
$expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' .
- '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' .
- '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
- '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
- '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
- 'border: 1px solid '.$color .';'.
+ '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' .
+ 'border: 1px solid '.$color.';'.
'background-color: '.$color.';'.
- 'opacity: 0.8' .
+ 'opacity: 0.8;' .
+ 'color: #ffffff;'.
"}\n" .
'.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' .
'.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' .
'border: 1px solid '.$color.';'.
'background-color: '.$color.';'.
'opacity: 1.0;' .
+ 'color: #ffffff;'.
+ "}\n" .
+ '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
+ '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
+ '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ 'border: 1px solid '.$color.';'.
+ 'background-color: '.$color.';'.
+ 'opacity: 0.4;' .
+ 'color: #ffffff;'.
"}\n";
$expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n";
$expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' .
@@ -789,19 +810,26 @@ class ThemingControllerTest extends TestCase {
'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton('#555555').'\');' .
"}\n";
$expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' .
- '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' .
- '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
- '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
- '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
- 'border: 1px solid #555555;'.
- 'background-color: #555555;'.
- 'opacity: 0.8' .
+ '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' .
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
+ 'opacity: 0.8;' .
+ 'color: #000000;'.
"}\n" .
'.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' .
'.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' .
- 'border: 1px solid #555555;'.
- 'background-color: #555555;'.
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
'opacity: 1.0;' .
+ 'color: #000000;'.
+ "}\n" .
+ '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' .
+ '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' .
+ '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' .
+ 'border: 1px solid '.$elementColor.';'.
+ 'background-color: '.$elementColor.';'.
+ 'opacity: 0.4;' .
+ 'color: #000000;'.
"}\n";
$expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n";
$expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' .
diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php
index f63d5cd8f2c..083f4bb0518 100644
--- a/core/Controller/LoginController.php
+++ b/core/Controller/LoginController.php
@@ -241,12 +241,26 @@ class LoginController extends Controller {
if ($this->twoFactorManager->isTwoFactorAuthenticated($loginResult)) {
$this->twoFactorManager->prepareTwoFactorLogin($loginResult);
+
+ $providers = $this->twoFactorManager->getProviders($loginResult);
+ if (count($providers) === 1) {
+ // Single provider, hence we can redirect to that provider's challenge page directly
+ /* @var $provider IProvider */
+ $provider = array_pop($providers);
+ $url = 'core.TwoFactorChallenge.showChallenge';
+ $urlParams = [
+ 'challengeProviderId' => $provider->getId(),
+ ];
+ } else {
+ $url = 'core.TwoFactorChallenge.selectChallenge';
+ $urlParams = [];
+ }
+
if (!is_null($redirect_url)) {
- return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge', [
- 'redirect_url' => $redirect_url
- ]));
+ $urlParams['redirect_url'] = $redirect_url;
}
- return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
+
+ return new RedirectResponse($this->urlGenerator->linkToRoute($url, $urlParams));
}
return $this->generateRedirect($redirect_url);
diff --git a/lib/private/Repair/RepairUnmergedShares.php b/lib/private/Repair/RepairUnmergedShares.php
index 353877bb873..d57bc3779f8 100644
--- a/lib/private/Repair/RepairUnmergedShares.php
+++ b/lib/private/Repair/RepairUnmergedShares.php
@@ -93,7 +93,7 @@ class RepairUnmergedShares implements IRepairStep {
*/
$query = $this->connection->getQueryBuilder();
$query
- ->select('item_source', 'id', 'file_target', 'permissions', 'parent', 'share_type')
+ ->select('item_source', 'id', 'file_target', 'permissions', 'parent', 'share_type', 'stime')
->from('share')
->where($query->expr()->eq('share_type', $query->createParameter('shareType')))
->andWhere($query->expr()->in('share_with', $query->createParameter('shareWiths')))
@@ -148,6 +148,52 @@ class RepairUnmergedShares implements IRepairStep {
return $groupedShares;
}
+ private function isPotentialDuplicateName($name) {
+ return (preg_match('/\(\d+\)(\.[^\.]+)?$/', $name) === 1);
+ }
+
+ /**
+ * Decide on the best target name based on all group shares and subshares,
+ * goal is to increase the likeliness that the chosen name matches what
+ * the user is expecting.
+ *
+ * For this, we discard the entries with parenthesis "(2)".
+ * In case the user also renamed the duplicates to a legitimate name, this logic
+ * will still pick the most recent one as it's the one the user is most likely to
+ * remember renaming.
+ *
+ * If no suitable subshare is found, use the least recent group share instead.
+ *
+ * @param array $groupShares group share entries
+ * @param array $subShares sub share entries
+ *
+ * @return string chosen target name
+ */
+ private function findBestTargetName($groupShares, $subShares) {
+ $pickedShare = null;
+ // sort by stime, this also properly sorts the direct user share if any
+ @usort($subShares, function($a, $b) {
+ return ((int)$a['stime'] - (int)$b['stime']);
+ });
+
+ foreach ($subShares as $subShare) {
+ // skip entries that have parenthesis with numbers
+ if ($this->isPotentialDuplicateName($subShare['file_target'])) {
+ continue;
+ }
+ // pick any share found that would match, the last being the most recent
+ $pickedShare = $subShare;
+ }
+
+ // no suitable subshare found
+ if ($pickedShare === null) {
+ // use least recent group share target instead
+ $pickedShare = $groupShares[0];
+ }
+
+ return $pickedShare['file_target'];
+ }
+
/**
* Fix the given received share represented by the set of group shares
* and matching sub shares
@@ -171,7 +217,7 @@ class RepairUnmergedShares implements IRepairStep {
return false;
}
- $targetPath = $groupShares[0]['file_target'];
+ $targetPath = $this->findBestTargetName($groupShares, $subShares);
// check whether the user opted out completely of all subshares
$optedOut = true;
diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php
index 417a60a9e5f..ff50ac98fbd 100644
--- a/tests/Core/Controller/LoginControllerTest.php
+++ b/tests/Core/Controller/LoginControllerTest.php
@@ -505,7 +505,7 @@ class LoginControllerTest extends TestCase {
$this->assertEquals($expected, $this->loginController->tryLogin('Jane', $password, $originalUrl));
}
- public function testLoginWithTwoFactorEnforced() {
+ public function testLoginWithOneTwoFactorProvider() {
/** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */
$user = $this->getMockBuilder('\OCP\IUser')->getMock();
$user->expects($this->any())
@@ -513,6 +513,7 @@ class LoginControllerTest extends TestCase {
->will($this->returnValue('john'));
$password = 'secret';
$challengeUrl = 'challenge/url';
+ $provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
$this->request
->expects($this->exactly(2))
@@ -547,6 +548,79 @@ class LoginControllerTest extends TestCase {
$this->twoFactorManager->expects($this->once())
->method('prepareTwoFactorLogin')
->with($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProviders')
+ ->with($user)
+ ->will($this->returnValue([$provider]));
+ $provider->expects($this->once())
+ ->method('getId')
+ ->will($this->returnValue('u2f'));
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('core.TwoFactorChallenge.showChallenge', [
+ 'challengeProviderId' => 'u2f',
+ ])
+ ->will($this->returnValue($challengeUrl));
+ $this->config->expects($this->once())
+ ->method('deleteUserValue')
+ ->with('john', 'core', 'lostpassword');
+
+ $expected = new RedirectResponse($challengeUrl);
+ $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', $password, null));
+ }
+
+ public function testLoginWithMultpleTwoFactorProviders() {
+ /** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */
+ $user = $this->getMockBuilder('\OCP\IUser')->getMock();
+ $user->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue('john'));
+ $password = 'secret';
+ $challengeUrl = 'challenge/url';
+ $provider1 = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
+ $provider2 = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
+
+ $this->request
+ ->expects($this->exactly(2))
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('getDelay')
+ ->with('192.168.0.1')
+ ->willReturn(200);
+ $this->userManager->expects($this->once())
+ ->method('checkPassword')
+ ->will($this->returnValue($user));
+ $this->userSession->expects($this->once())
+ ->method('login')
+ ->with('john@doe.com', $password);
+ $this->userSession->expects($this->once())
+ ->method('createSessionToken')
+ ->with($this->request, $user->getUID(), 'john@doe.com', $password);
+ $this->twoFactorManager->expects($this->once())
+ ->method('isTwoFactorAuthenticated')
+ ->with($user)
+ ->will($this->returnValue(true));
+ $this->twoFactorManager->expects($this->once())
+ ->method('prepareTwoFactorLogin')
+ ->with($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProviders')
+ ->with($user)
+ ->will($this->returnValue([$provider1, $provider2]));
+ $provider1->expects($this->never())
+ ->method('getId');
+ $provider2->expects($this->never())
+ ->method('getId');
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('core.TwoFactorChallenge.selectChallenge')
diff --git a/tests/lib/Repair/RepairUnmergedSharesTest.php b/tests/lib/Repair/RepairUnmergedSharesTest.php
index fe9b3e5b96f..7b9d2579389 100644
--- a/tests/lib/Repair/RepairUnmergedSharesTest.php
+++ b/tests/lib/Repair/RepairUnmergedSharesTest.php
@@ -28,6 +28,8 @@ use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
use Test\TestCase;
use OC\Share20\DefaultShareProvider;
+use OCP\IUserManager;
+use OCP\IGroupManager;
/**
* Tests for repairing invalid shares
@@ -44,6 +46,15 @@ class RepairUnmergedSharesTest extends TestCase {
/** @var \OCP\IDBConnection */
private $connection;
+ /** @var int */
+ private $lastShareTime;
+
+ /** @var IUserManager */
+ private $userManager;
+
+ /** @var IGroupManager */
+ private $groupManager;
+
protected function setUp() {
parent::setUp();
@@ -58,42 +69,14 @@ class RepairUnmergedSharesTest extends TestCase {
$this->connection = \OC::$server->getDatabaseConnection();
$this->deleteAllShares();
- $user1 = $this->getMock('\OCP\IUser');
- $user1->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('user1'));
-
- $user2 = $this->getMock('\OCP\IUser');
- $user2->expects($this->any())
- ->method('getUID')
- ->will($this->returnValue('user2'));
-
- $users = [$user1, $user2];
-
- $groupManager = $this->getMock('\OCP\IGroupManager');
- $groupManager->expects($this->any())
- ->method('getUserGroupIds')
- ->will($this->returnValueMap([
- // owner
- [$user1, ['samegroup1', 'samegroup2']],
- // recipient
- [$user2, ['recipientgroup1', 'recipientgroup2']],
- ]));
+ $this->userManager = $this->getMock('\OCP\IUserManager');
+ $this->groupManager = $this->getMock('\OCP\IGroupManager');
- $userManager = $this->getMock('\OCP\IUserManager');
- $userManager->expects($this->once())
- ->method('countUsers')
- ->will($this->returnValue([2]));
- $userManager->expects($this->once())
- ->method('callForAllUsers')
- ->will($this->returnCallback(function(\Closure $closure) use ($users) {
- foreach ($users as $user) {
- $closure($user);
- }
- }));
+ // used to generate incremental stimes
+ $this->lastShareTime = time();
/** @var \OCP\IConfig $config */
- $this->repair = new RepairUnmergedShares($config, $this->connection, $userManager, $groupManager);
+ $this->repair = new RepairUnmergedShares($config, $this->connection, $this->userManager, $this->groupManager);
}
protected function tearDown() {
@@ -108,6 +91,7 @@ class RepairUnmergedSharesTest extends TestCase {
}
private function createShare($type, $sourceId, $recipient, $targetName, $permissions, $parentId = null) {
+ $this->lastShareTime += 100;
$qb = $this->connection->getQueryBuilder();
$values = [
'share_type' => $qb->expr()->literal($type),
@@ -119,7 +103,7 @@ class RepairUnmergedSharesTest extends TestCase {
'file_source' => $qb->expr()->literal($sourceId),
'file_target' => $qb->expr()->literal($targetName),
'permissions' => $qb->expr()->literal($permissions),
- 'stime' => $qb->expr()->literal(time()),
+ 'stime' => $qb->expr()->literal($this->lastShareTime),
];
if ($parentId !== null) {
$values['parent'] = $qb->expr()->literal($parentId);
@@ -204,7 +188,7 @@ class RepairUnmergedSharesTest extends TestCase {
[
// #2 bogus share
// - outsider shares with group1, group2
- // - one subshare for each group share
+ // - one subshare for each group share, both with parenthesis
// - but the targets do not match when grouped
[
[Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31],
@@ -218,7 +202,7 @@ class RepairUnmergedSharesTest extends TestCase {
[
['/test', 31],
['/test', 31],
- // reset to original name
+ // reset to original name as the sub-names have parenthesis
['/test', 31],
['/test', 31],
// leave unrelated alone
@@ -228,6 +212,54 @@ class RepairUnmergedSharesTest extends TestCase {
[
// #3 bogus share
// - outsider shares with group1, group2
+ // - one subshare for each group share, both renamed manually
+ // - but the targets do not match when grouped
+ [
+ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31],
+ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31],
+ // child of the previous ones
+ [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test_renamed (1 legit paren)', 31, 0],
+ [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test_renamed (2 legit paren)', 31, 1],
+ // different unrelated share
+ [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31],
+ ],
+ [
+ ['/test', 31],
+ ['/test', 31],
+ // reset to less recent subshare name
+ ['/test_renamed (2 legit paren)', 31],
+ ['/test_renamed (2 legit paren)', 31],
+ // leave unrelated alone
+ ['/test (4)', 31],
+ ]
+ ],
+ [
+ // #4 bogus share
+ // - outsider shares with group1, group2
+ // - one subshare for each group share, one with parenthesis
+ // - but the targets do not match when grouped
+ [
+ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31],
+ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31],
+ // child of the previous ones
+ [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (2)', 31, 0],
+ [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test_renamed', 31, 1],
+ // different unrelated share
+ [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31],
+ ],
+ [
+ ['/test', 31],
+ ['/test', 31],
+ // reset to less recent subshare name but without parenthesis
+ ['/test_renamed', 31],
+ ['/test_renamed', 31],
+ // leave unrelated alone
+ ['/test (4)', 31],
+ ]
+ ],
+ [
+ // #5 bogus share
+ // - outsider shares with group1, group2
// - one subshare for each group share
// - first subshare not renamed (as in real world scenario)
// - but the targets do not match when grouped
@@ -251,7 +283,7 @@ class RepairUnmergedSharesTest extends TestCase {
]
],
[
- // #4 bogus share:
+ // #6 bogus share:
// - outsider shares with group1, group2
// - one subshare for each group share
// - non-matching targets
@@ -276,7 +308,7 @@ class RepairUnmergedSharesTest extends TestCase {
]
],
[
- // #5 bogus share:
+ // #7 bogus share:
// - outsider shares with group1, group2
// - one subshare for each group share
// - non-matching targets
@@ -301,7 +333,7 @@ class RepairUnmergedSharesTest extends TestCase {
]
],
[
- // #6 bogus share:
+ // #8 bogus share:
// - outsider shares with group1, group2 and also user2
// - one subshare for each group share
// - one extra share entry for direct share to user2
@@ -329,7 +361,7 @@ class RepairUnmergedSharesTest extends TestCase {
]
],
[
- // #7 bogus share:
+ // #9 bogus share:
// - outsider shares with group1 and also user2
// - no subshare at all
// - one extra share entry for direct share to user2
@@ -350,7 +382,7 @@ class RepairUnmergedSharesTest extends TestCase {
]
],
[
- // #8 legitimate share with own group:
+ // #10 legitimate share with own group:
// - insider shares with both groups the user is already in
// - no subshares in this case
[
@@ -368,7 +400,7 @@ class RepairUnmergedSharesTest extends TestCase {
]
],
[
- // #9 legitimate shares:
+ // #11 legitimate shares:
// - group share with same group
// - group share with other group
// - user share where recipient renamed
@@ -392,7 +424,7 @@ class RepairUnmergedSharesTest extends TestCase {
]
],
[
- // #10 legitimate share:
+ // #12 legitimate share:
// - outsider shares with group and user directly with different permissions
// - no subshares
// - same targets
@@ -410,6 +442,42 @@ class RepairUnmergedSharesTest extends TestCase {
['/test (4)', 31],
]
],
+ [
+ // #13 bogus share:
+ // - outsider shares with group1, user2 and then group2
+ // - user renamed share as soon as it arrived before the next share (order)
+ // - one subshare for each group share
+ // - one extra share entry for direct share to user2
+ // - non-matching targets
+ [
+ // first share with group
+ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31],
+ // recipient renames
+ [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/first', 31, 0],
+ // then direct share, user renames too
+ [Constants::SHARE_TYPE_USER, 123, 'user2', '/second', 31],
+ // another share with the second group
+ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31],
+ // use renames it
+ [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/third', 31, 1],
+ // different unrelated share
+ [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (5)', 31],
+ ],
+ [
+ // group share with group1 left alone
+ ['/test', 31],
+ // first subshare repaired
+ ['/third', 31],
+ // direct user share repaired
+ ['/third', 31],
+ // group share with group2 left alone
+ ['/test', 31],
+ // second subshare repaired
+ ['/third', 31],
+ // leave unrelated alone
+ ['/test (5)', 31],
+ ]
+ ],
];
}
@@ -419,6 +487,38 @@ class RepairUnmergedSharesTest extends TestCase {
* @dataProvider sharesDataProvider
*/
public function testMergeGroupShares($shares, $expectedShares) {
+ $user1 = $this->getMock('\OCP\IUser');
+ $user1->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue('user1'));
+
+ $user2 = $this->getMock('\OCP\IUser');
+ $user2->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue('user2'));
+
+ $users = [$user1, $user2];
+
+ $this->groupManager->expects($this->any())
+ ->method('getUserGroupIds')
+ ->will($this->returnValueMap([
+ // owner
+ [$user1, ['samegroup1', 'samegroup2']],
+ // recipient
+ [$user2, ['recipientgroup1', 'recipientgroup2']],
+ ]));
+
+ $this->userManager->expects($this->once())
+ ->method('countUsers')
+ ->will($this->returnValue([2]));
+ $this->userManager->expects($this->once())
+ ->method('callForAllUsers')
+ ->will($this->returnCallback(function(\Closure $closure) use ($users) {
+ foreach ($users as $user) {
+ $closure($user);
+ }
+ }));
+
$shareIds = [];
foreach ($shares as $share) {
@@ -445,5 +545,30 @@ class RepairUnmergedSharesTest extends TestCase {
$this->assertEquals($expectedShare[1], $share['permissions']);
}
}
+
+ public function duplicateNamesProvider() {
+ return [
+ // matching
+ ['filename (1).txt', true],
+ ['folder (2)', true],
+ ['filename (1)(2).txt', true],
+ // non-matching
+ ['filename ().txt', false],
+ ['folder ()', false],
+ ['folder (1x)', false],
+ ['folder (x1)', false],
+ ['filename (a)', false],
+ ['filename (1).', false],
+ ['filename (1).txt.txt', false],
+ ['filename (1)..txt', false],
+ ];
+ }
+
+ /**
+ * @dataProvider duplicateNamesProvider
+ */
+ public function testIsPotentialDuplicateName($name, $expectedResult) {
+ $this->assertEquals($expectedResult, $this->invokePrivate($this->repair, 'isPotentialDuplicateName', [$name]));
+ }
}