Compare commits

...

79 Commits

Author SHA1 Message Date
Roeland Jago Douma
615b994816 Merge pull request #23071 from nextcloud/version/20.0.0/final
20 final
2020-10-02 18:42:10 +02:00
Roeland Jago Douma
c2eb39d662 Merge pull request #23143 from nextcloud/backport/23114/stable20
[stable20]  Show icon only with dnd status in the message
2020-10-02 18:33:17 +02:00
Joas Schilling
d2bd0664be Show the full status and icon all the time
Signed-off-by: Joas Schilling <coding@schilljs.com>
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
2020-10-02 17:18:43 +02:00
Julius Härtl
d3d7209be9 Show icon only with dnd status in the message
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2020-10-02 17:13:26 +02:00
Roeland Jago Douma
2c149fbd9a Merge pull request #23141 from nextcloud/backport/23136/stable20
[stable20] Mark all compiled JS as binary
2020-10-02 17:01:03 +02:00
Roeland Jago Douma
998ab15206 Merge pull request #23132 from nextcloud/backport/23130/stable20
[stable20] Move online status into modal
2020-10-02 16:58:35 +02:00
Joas Schilling
d204e5855c Mark all compiled JS as binary
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-10-02 13:41:55 +00:00
John Molakvoæ (skjnldsv)
bb662c20fe Bump @nextcloud/vue to v2.6.8
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2020-10-02 13:33:43 +02:00
John Molakvoæ (skjnldsv)
097d62049d Fix subline hint
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2020-10-02 13:22:42 +02:00
Joas Schilling
be778c94e1 Show the subline
Signed-off-by: Joas Schilling <coding@schilljs.com>
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
2020-10-02 12:50:29 +02:00
Joas Schilling
ac3a32f305 Set status is also there now
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-10-02 09:26:53 +00:00
Joas Schilling
28ae039588 Update @nextcloud/vue to 2.6.7
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-10-02 09:26:52 +00:00
Jan C. Borchardt
1f0a5aeae3 Status: Add subline for Invisible to explain it properly
Signed-off-by: Jan C. Borchardt <hey@jancborchardt.net>
2020-10-02 09:26:50 +00:00
Jan C. Borchardt
741ebf5177 Enable scrollbar for too long content, same way as in Dashboard customize
Signed-off-by: Jan C. Borchardt <hey@jancborchardt.net>
2020-10-02 09:26:50 +00:00
John Molakvoæ (skjnldsv)
4817005733 Move online status into modal
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2020-10-02 09:26:50 +00:00
Roeland Jago Douma
fa4cd4435b Merge pull request #23089 from nextcloud/backport/23074/stable20
[stable20] Do not match sharees on an empty email address
2020-10-01 19:29:39 +02:00
Roeland Jago Douma
61a0069dd5 Merge pull request #23115 from nextcloud/backport/23108/stable20
[stable20] Reset the user status when clearing the custom message
2020-10-01 19:23:51 +02:00
Roeland Jago Douma
2433550eff Merge pull request #23121 from nextcloud/backport/23112/stable20
[stable20] Increase the timeout of statuses
2020-10-01 15:09:51 +02:00
Joas Schilling
d7d805ef79 Increase the timeout of statuses
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-10-01 09:45:46 +00:00
Roeland Jago Douma
612306d290 Merge pull request #23116 from nextcloud/backport/23113/stable20
[stable20] Change wording from 'custom status' to 'status message'
2020-10-01 08:29:27 +02:00
Jan C. Borchardt
b66f5c55e5 Change wording from 'custom status' to 'status message'
Signed-off-by: Jan C. Borchardt <hey@jancborchardt.net>
Signed-off-by: Gary Kim <gary@garykim.dev>
2020-09-30 22:26:57 -04:00
Joas Schilling
413a6042f3 Reset the user status when clearing the custom message
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-09-30 17:27:14 +00:00
Roeland Jago Douma
f0dc0d1347 Merge pull request #23098 from nextcloud/backport/22999/stable20
[stable20] Adjust scroll container height to make it a proper boundary element for actions
2020-09-30 13:48:05 +02:00
Roeland Jago Douma
6b8356ce35 Merge pull request #23095 from nextcloud/backport/23043/stable20
[stable20] Avoid crash when unauthenticated users make weather-related requests
2020-09-29 21:35:17 +02:00
Julius Härtl
143c6356c5 Adjust scroll container height to make it a proper boundary element for actions
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2020-09-29 16:43:41 +00:00
Roeland Jago Douma
2e87668f77 Merge pull request #23092 from nextcloud/backport/23083/stable20
[stable20] Generate exception to log on php errors
2020-09-29 16:38:56 +02:00
Julien Veyssier
2364808913 avoid crash when unauthenticated users make weather-related requests, mention it in UI
Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
2020-09-29 12:18:55 +00:00
Roeland Jago Douma
f72ebcd956 Merge pull request #23085 from nextcloud/backport/23013/stable20
[stable20] Show federation and email results also with exact user match unless c…
2020-09-29 12:16:00 +02:00
Roeland Jago Douma
b1879c4fcb Merge pull request #23084 from nextcloud/backport/22983/stable20
[stable20] Sync all users to the system addresssbook
2020-09-29 11:52:15 +02:00
Julius Härtl
05fa5e4d9e Generate exception to log on php errors
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2020-09-29 08:17:21 +00:00
Christoph Wurst
851333edab Do not match sharees on an empty email address
When asking for sharees we compare not only UID and displayname but also
the email address. And if that matches we return the sharee as an exact
match. This logic had a flaw as in that it also matched the empty string
to users with no email address.

This is most noticeable when you disable sharee enumeration and open the
ownership transfer dialog. It suggested other users of the instance
before. This has stopped now.

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
2020-09-29 07:29:43 +00:00
Joas Schilling
71b33fb87a Show federation and email results also with exact user match unless containing @
Before when you have a user "smith" and a federated user "smith@example.com"
you could see the federation result with "smit" but not with "smith" anymore.
With most LDAP configurations and local backend setups this is disturbing and
causes issues.
The idea of not showing the email and federation on a matching user was with:
Local user registered with "smith@example.com" user id and having that same
email / cloud id in your contacts addressbook. So we now only hide those
"side results" when the search does contain an @

Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-09-29 07:16:12 +00:00
Joas Schilling
6365e7e162 Sync all users to the system addresssbook
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-09-29 07:15:49 +00:00
Roeland Jago Douma
fb426c90b7 Merge pull request #23072 from nextcloud/backport/23051/stable20
[stable20] Fix app text going too far down on hover/focus
2020-09-28 12:40:18 +02:00
Roeland Jago Douma
15ff980583 Merge pull request #23029 from nextcloud/backport/23024/stable20
[stable20] Add occ command to set theming values
2020-09-28 12:24:23 +02:00
Jan C. Borchardt
d358f9dddf Fix app text going too far down on hover/focus
Signed-off-by: Jan C. Borchardt <hey@jancborchardt.net>
2020-09-28 07:34:29 +00:00
Roeland Jago Douma
7496a10227 Merge pull request #23032 from nextcloud/backport/23015/stable20
[stable20] Log slow dashboard widgets
2020-09-28 09:33:56 +02:00
Roeland Jago Douma
7630052a60 Merge pull request #23048 from nextcloud/backport/23034/stable20
[stable20] Fix numeric folders throwing on markDirty
2020-09-28 09:32:40 +02:00
Roeland Jago Douma
5ab7392d56 20 final
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
2020-09-28 09:28:36 +02:00
Joas Schilling
aefbf4c01d Fix numeric folders throwing on markDirty
TypeError: strpos() expects parameter 1 to be string, int given

The problem is that in cacheNode() we strip of any slashes, so
a folder "0/" will be trimmed to "0" and be used as an array key.
Since PHP automatically casts numeric array keys to integers,
you afterwards get $nodePath as int(0). Since it's now a number,
the strpos() function does not accept it anymore. Simply casting
$nodePath to a string again in the foreach solves the issue

Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-09-25 13:14:49 +00:00
Roeland Jago Douma
d81b4e2ff7 Merge pull request #23030 from nextcloud/backport/22948/stable20
[stable20] Add more integration tests for "files:transfer-ownership" command
2020-09-24 21:32:28 +02:00
Roeland Jago Douma
3320d8ecf1 Merge pull request #23038 from nextcloud/fix-running-video-verification-integration-tests-in-drone-in-stable20
[stable20] Fix running video verification integration tests in Drone
2020-09-24 21:30:23 +02:00
Daniel Calviño Sánchez
a1d3213e7d Fix running video verification integration tests in Drone in stable20
In order to run the video verification integration tests the Talk app
needs to be cloned in a branch compatible with the server.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
2020-09-24 16:52:04 +02:00
Joas Schilling
5475bb4083 Log a warning if a "lazy" initial state loads longer than 1 second
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-09-24 13:00:19 +00:00
Joas Schilling
87f8e1e366 Log an error if a dashboard widget loads longer than 1 second
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-09-24 13:00:19 +00:00
Daniel Calviño Sánchez
0dd18e0356 Add integration tests to check that only the given path is transferred
Until recently (it was fixed in ac2999a26a) when a path was transferred
other shares with the target user were removed, so a test was added to
ensure that it does not happen again.

Besides that a test to ensure that other files with the target user are
not transferred was added too (it did not fail before, but seemed
convenient to have that covered too :-) ).

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
2020-09-24 12:47:47 +00:00
Daniel Calviño Sánchez
bf9a24efbe Add integration tests for transferring files of a user with a risky name
The files:transfer-ownership performs a sanitization of users with
"risky" display names (including characters like "\" or "/").

In order to allow (escaped) double quotes in the display name the
regular expression used in the "user XXX with displayname YYY exists"
step had to be adjusted.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
2020-09-24 12:47:47 +00:00
Daniel Calviño Sánchez
87b9dbdb56 Add integration test for transferring the path of a single file
Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
2020-09-24 12:47:47 +00:00
Julius Härtl
19390a4b5e Fix tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2020-09-24 12:38:53 +00:00
Julius Härtl
99b25ef3fe Add occ command to set theming values
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2020-09-24 12:38:53 +00:00
Roeland Jago Douma
d247f198a9 Merge pull request #23000 from nextcloud/version/20.0.0/RC2
20 RC2
2020-09-24 13:41:12 +02:00
Roeland Jago Douma
e29b5c6d92 Merge pull request #23026 from nextcloud/backport/23014/stable20
[stable20] Make 'Reasons to use Nextcloud' button translatable, fix #22977
2020-09-24 11:31:39 +02:00
Roeland Jago Douma
fa0f815dda Merge pull request #23001 from nextcloud/backport/22940/stable20
[stable20] Never copy the share link when the password is forced
2020-09-24 09:37:22 +02:00
Jan C. Borchardt
2be71de5a1 Make 'Reasons to use Nextcloud' button translatable, fix #22977
Signed-off-by: Jan C. Borchardt <hey@jancborchardt.net>
2020-09-24 07:26:18 +00:00
Roeland Jago Douma
724276c7a7 Merge pull request #23018 from nextcloud/backport/23016/stable20
[stable20] Don't log a known shared section
2020-09-24 09:24:42 +02:00
Joas Schilling
26603c7cdd Don't log "duplicate section" for the shared "connected-accounts" section
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-09-23 11:03:50 +00:00
Joas Schilling
7edced3807 Merge pull request #23009 from nextcloud/backport/23008/stable20
[stable20] Add padding to the empty content and center it
2020-09-23 10:46:50 +02:00
Joas Schilling
3daddfce14 Add padding to the empty content and center it
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-09-22 19:16:30 +00:00
Joas Schilling
9838d54cb5 Never copy the share link when the password is forced
Signed-off-by: Joas Schilling <coding@schilljs.com>
2020-09-22 11:45:58 +00:00
Roeland Jago Douma
46babff37b Merge pull request #22928 from nextcloud/backport/22915/stable20
[stable20] improve handling of out of space errors for smb
2020-09-22 13:08:21 +02:00
Roeland Jago Douma
ff8eb8dfa2 20 RC2
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
2020-09-22 13:05:52 +02:00
John Molakvoæ
289bc8e345 Merge pull request #22946 from nextcloud/backport/22924/stable20
[stable20] Make sure most app names don’t ellipsize, fix #22845, fix #22219
2020-09-19 00:02:01 +02:00
Morris Jobke
a0e8a78945 Fix transifex name of dashboard app
Signed-off-by: Morris Jobke <hey@morrisjobke.de>
2020-09-18 20:47:38 +02:00
Morris Jobke
065f3e125e Merge pull request #22950 from nextcloud/backport/22949/stable20
[stable20] Add transifex config for all new apps
2020-09-18 20:43:29 +02:00
Morris Jobke
bf58dcb247 Add transifex config for all new apps
Signed-off-by: Morris Jobke <hey@morrisjobke.de>
2020-09-18 18:42:09 +00:00
Jan C. Borchardt
ada7ad6930 Make sure most app names don’t ellipsize, fix #22845, fix #22219
Signed-off-by: Jan C. Borchardt <hey@jancborchardt.net>
2020-09-18 16:18:48 +00:00
Morris Jobke
5d81cb36b5 Merge pull request #22938 from nextcloud/backport/22911/stable20
[stable20] Allow to run occ preview:repair in parallel
2020-09-18 18:14:29 +02:00
Morris Jobke
03d00afe31 Merge pull request #22935 from nextcloud/backport/22868/stable20
[stable20] Fix/unified search papercuts
2020-09-18 18:14:17 +02:00
Morris Jobke
b35daf665f Migrate verbose messages to inline syntax of writeln()
Signed-off-by: Morris Jobke <hey@morrisjobke.de>
2020-09-18 11:13:52 +00:00
Morris Jobke
55393939ce Show lock messages only in verbose mode
Signed-off-by: Morris Jobke <hey@morrisjobke.de>
2020-09-18 11:13:52 +00:00
Morris Jobke
e9e5a02d7c Allow to run occ preview:repair in parallel
Signed-off-by: Morris Jobke <hey@morrisjobke.de>
2020-09-18 11:13:52 +00:00
John Molakvoæ (skjnldsv)
7ad973494a Prevent empty search placeholder
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2020-09-18 09:36:41 +00:00
John Molakvoæ (skjnldsv)
5646144fae Build assets and fix unified search event syntax
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2020-09-18 09:36:40 +00:00
John Molakvoæ (skjnldsv)
839f597921 Properly show loading state if there are still pending requests
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2020-09-18 09:36:39 +00:00
John Molakvoæ (skjnldsv)
3c6319f275 Properly use form role=search and unify reset button
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2020-09-18 09:36:39 +00:00
John Molakvoæ (skjnldsv)
2672f5da59 Fix loading error catch
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2020-09-18 09:36:39 +00:00
Roeland Jago Douma
2000e2faa5 Merge pull request #22932 from nextcloud/backport/22925/stable20
[stable20] Dashboard: Fix accessibility skip links
2020-09-18 11:32:38 +02:00
Jan C. Borchardt
0504873a8a Dashboard: Fix accessibility skip links
Signed-off-by: Jan C. Borchardt <hey@jancborchardt.net>
2020-09-18 06:35:28 +00:00
Robin Appelman
eba4723428 improve handling of out of space errors for smb
Signed-off-by: Robin Appelman <robin@icewind.nl>
2020-09-17 18:47:33 +00:00
137 changed files with 1352 additions and 580 deletions

View File

@@ -1350,7 +1350,7 @@ steps:
commands:
# JavaScript files are not used in integration tests so it is not needed to
# build them.
- git clone --depth 1 https://github.com/nextcloud/spreed apps/spreed
- git clone --branch stable20 --depth 1 https://github.com/nextcloud/spreed apps/spreed
- name: integration-sharing-v1-video-verification
image: nextcloudci/integration-php7.3:integration-php7.3-2
commands:

18
.gitattributes vendored
View File

@@ -1,8 +1,8 @@
/core/js/dist/*.js binary
/core/js/dist/*.js.map binary
/apps/accessibility/js/accessibility.js binary
/apps/accessibility/js/accessibility.js.map binary
/apps/accessibility/js/*.js binary
/apps/accessibility/js/*.js.map binary
/apps/comments/js/*.js binary
/apps/comments/js/*.js.map binary
/apps/dashboard/js/*.js binary
@@ -11,10 +11,12 @@
/apps/files/js/dist/*.js.map binary
/apps/files_sharing/js/dist/*.js binary
/apps/files_sharing/js/dist/*.js.map binary
/apps/files_versions/js/files_versions.js binary
/apps/files_versions/js/files_versions.js.map binary
/apps/oauth2/js/oauth2.js binary
/apps/oauth2/js/oauth2.js.map binary
/apps/files_trashbin/js/*.js binary
/apps/files_trashbin/js/*.js.map binary
/apps/files_versions/js/*.js binary
/apps/files_versions/js/*.js.map binary
/apps/oauth2/js/*.js binary
/apps/oauth2/js/*.js.map binary
/apps/settings/js/vue* binary
/apps/systemtags/js/systemtags.js binary
/apps/systemtags/js/systemtags.js.map binary
@@ -22,5 +24,9 @@
/apps/twofactor_backupcodes/js/*.js.map binary
/apps/updatenotification/js/updatenotification.js binary
/apps/updatenotification/js/updatenotification.js.map binary
/apps/user_status/js/*.js binary
/apps/user_status/js/*.js.map binary
/apps/weather_status/js/*.js binary
/apps/weather_status/js/*.js.map binary
/apps/workflowengine/js/*.js binary
/apps/workflowengine/js/*.js.map binary

View File

@@ -133,3 +133,51 @@ file_filter = translationfiles/<lang>/accessibility.po
source_file = translationfiles/templates/accessibility.pot
source_lang = en
type = PO
[nextcloud.provisioning_api]
file_filter = translationfiles/<lang>/provisioning_api.po
source_file = translationfiles/templates/provisioning_api.pot
source_lang = en
type = PO
[nextcloud.lookup_server_connector]
file_filter = translationfiles/<lang>/lookup_server_connector.po
source_file = translationfiles/templates/lookup_server_connector.pot
source_lang = en
type = PO
[nextcloud.dashboard-shipped-with-server]
file_filter = translationfiles/<lang>/dashboard.po
source_file = translationfiles/templates/dashboard.pot
source_lang = en
type = PO
[nextcloud.contactsinteraction]
file_filter = translationfiles/<lang>/contactsinteraction.po
source_file = translationfiles/templates/contactsinteraction.pot
source_lang = en
type = PO
[nextcloud.cloud_federation_api]
file_filter = translationfiles/<lang>/cloud_federation_api.po
source_file = translationfiles/templates/cloud_federation_api.pot
source_lang = en
type = PO
[nextcloud.admin_audit]
file_filter = translationfiles/<lang>/admin_audit.po
source_file = translationfiles/templates/admin_audit.pot
source_lang = en
type = PO
[nextcloud.user_status]
file_filter = translationfiles/<lang>/user_status.po
source_file = translationfiles/templates/user_status.pot
source_lang = en
type = PO
[nextcloud.weather_status]
file_filter = translationfiles/<lang>/weather_status.po
source_file = translationfiles/templates/weather_status.pot
source_lang = en
type = PO

View File

View File

View File

View File

@@ -1,3 +1,12 @@
// Suppress "Skip to navigation of app" link since the app does not have a navigation
.skip-navigation:not(.skip-content) {
display: none;
}
// Fix position of "Skip to main content" link since the other link is gone
.skip-navigation.skip-content {
left: 3px;
}
#header {
background: transparent !important;
--color-header: rgba(24, 24, 24, 1);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

View File

@@ -215,6 +215,7 @@ export default {
},
mounted() {
this.updateGlobalStyles()
this.updateSkipLink()
window.addEventListener('scroll', this.handleScroll)
setInterval(() => {
@@ -321,6 +322,10 @@ export default {
document.body.classList.remove('dashboard--dark')
}
},
updateSkipLink() {
// Make sure "Skip to main content" link points to the app content
document.getElementsByClassName('skip-navigation')[0].setAttribute('href', '#app-dashboard')
},
updateStatusCheckbox(app, checked) {
if (checked) {
this.enableStatus(app)

View File

@@ -322,7 +322,7 @@ class SyncService {
public function syncInstance(\Closure $progressCallback = null) {
$systemAddressBook = $this->getLocalSystemAddressBook();
$this->userManager->callForSeenUsers(function ($user) use ($systemAddressBook, $progressCallback) {
$this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback) {
$this->updateUser($user);
if (!is_null($progressCallback)) {
$progressCallback();

View File

@@ -38,4 +38,16 @@ class CachingTree extends Tree {
}
$this->cache[trim($path, '/')] = $node;
}
public function markDirty($path) {
// We don't care enough about sub-paths
// flushing the entire cache
$path = trim($path, '/');
foreach ($this->cache as $nodePath => $node) {
$nodePath = (string) $nodePath;
if ('' === $path || $nodePath == $path || 0 === strpos($nodePath, $path.'/')) {
unset($this->cache[$nodePath]);
}
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -44,6 +44,7 @@ use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\ForbiddenException;
use Icewind\SMB\Exception\InvalidArgumentException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\Exception\OutOfSpaceException;
use Icewind\SMB\Exception\TimedOutException;
use Icewind\SMB\IFileInfo;
use Icewind\SMB\Native\NativeServer;
@@ -57,6 +58,7 @@ use OC\Files\Filesystem;
use OC\Files\Storage\Common;
use OCA\Files_External\Lib\Notify\SMBNotifyHandler;
use OCP\Constants;
use OCP\Files\EntityTooLargeException;
use OCP\Files\Notify\IChange;
use OCP\Files\Notify\IRenameChange;
use OCP\Files\Storage\INotifyStorage;
@@ -497,6 +499,8 @@ class SMB extends Common implements INotifyStorage {
return false;
} catch (ForbiddenException $e) {
return false;
} catch (OutOfSpaceException $e) {
throw new EntityTooLargeException("not enough available space to create file", 0, $e);
} catch (ConnectException $e) {
$this->logger->logException($e, ['message' => 'Error while opening file']);
throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
@@ -538,6 +542,8 @@ class SMB extends Common implements INotifyStorage {
return true;
}
return false;
} catch (OutOfSpaceException $e) {
throw new EntityTooLargeException("not enough available space to create file", 0, $e);
} catch (ConnectException $e) {
$this->logger->logException($e, ['message' => 'Error while creating file']);
throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=151)}({151:function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}});
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=152)}({152:function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}});
//# sourceMappingURL=collaboration.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -708,7 +708,7 @@ export default {
// Execute the copy link method
// freshly created share component
// ! somehow does not works on firefox !
if (update || !this.config.enforcePasswordForPublicLink) {
if (!this.config.enforcePasswordForPublicLink) {
// Only copy the link when the password was not forced,
// otherwise the user needs to copy/paste the password before finishing the share.
component.copyLink()

View File

View File

@@ -1443,6 +1443,8 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
$grid-row-height: 60px;
$grid-col-min-width: 160px;
overflow-x: scroll;
min-height: 100%;
height: auto;
#app-content.user-list-grid {
display: grid;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -426,8 +426,8 @@ export default {
/**
* Register search
*/
subscribe('nextcloud:unified-search:search', this.search)
subscribe('nextcloud:unified-search:reset', this.resetSearch)
subscribe('nextcloud:unified-search.search', this.search)
subscribe('nextcloud:unified-search.reset', this.resetSearch)
/**
* If disabled group but empty, redirect
@@ -435,8 +435,8 @@ export default {
this.redirectIfDisabled()
},
beforeDestroy() {
unsubscribe('nextcloud:unified-search:search', this.search)
unsubscribe('nextcloud:unified-search:reset', this.resetSearch)
unsubscribe('nextcloud:unified-search.search', this.search)
unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
},
methods: {

View File

@@ -280,12 +280,12 @@ export default {
},
mounted() {
subscribe('nextcloud:unified-search:search', this.setSearch)
subscribe('nextcloud:unified-search:reset', this.resetSearch)
subscribe('nextcloud:unified-search.search', this.setSearch)
subscribe('nextcloud:unified-search.reset', this.resetSearch)
},
beforeDestroy() {
unsubscribe('nextcloud:unified-search:search', this.setSearch)
unsubscribe('nextcloud:unified-search:reset', this.resetSearch)
unsubscribe('nextcloud:unified-search.search', this.setSearch)
unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
},
methods: {

View File

@@ -1,6 +1,6 @@
<div class="section development-notice">
<p>
<a href="<?php p($_['reasons-use-nextcloud-pdf-link']); ?>" id="open-reasons-use-nextcloud-pdf" class="link-button icon-file" target="_blank">Reasons to use Nextcloud in your organization</a>
<a href="<?php p($_['reasons-use-nextcloud-pdf-link']); ?>" id="open-reasons-use-nextcloud-pdf" class="link-button icon-file" target="_blank"><?php p($l->t('Reasons to use Nextcloud in your organization'));?></a>
</p>
<p>
<?php print_unescaped(str_replace(

View File

@@ -25,4 +25,7 @@
<admin>OCA\Theming\Settings\Admin</admin>
<admin-section>OCA\Theming\Settings\Section</admin-section>
</settings>
<commands>
<command>OCA\Theming\Command\UpdateConfig</command>
</commands>
</info>

View File

@@ -0,0 +1,135 @@
<?php
/**
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Theming\Command;
use OCA\Theming\ImageManager;
use OCA\Theming\ThemingDefaults;
use OCP\IConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class UpdateConfig extends Command {
public const SUPPORTED_KEYS = [
'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color'
];
public const SUPPORTED_IMAGE_KEYS = [
'background', 'logo', 'favicon', 'logoheader'
];
private $themingDefaults;
private $imageManager;
private $config;
public function __construct(ThemingDefaults $themingDefaults, ImageManager $imageManager, IConfig $config) {
parent::__construct();
$this->themingDefaults = $themingDefaults;
$this->imageManager = $imageManager;
$this->config = $config;
}
protected function configure() {
$this
->setName('theming:config')
->setDescription('Set theming app config values')
->addArgument(
'key',
InputArgument::OPTIONAL,
'Key to update the theming app configuration (leave empty to get a list of all configured values)' . PHP_EOL .
'One of: ' . implode(', ', self::SUPPORTED_KEYS)
)
->addArgument(
'value',
InputArgument::OPTIONAL,
'Value to set (leave empty to obtain the current value)'
)
->addOption(
'reset',
'r',
InputOption::VALUE_NONE,
'Reset the given config key to default'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$key = $input->getArgument('key');
$value = $input->getArgument('value');
if ($key === null) {
$output->writeln('Current theming config:');
foreach (self::SUPPORTED_KEYS as $key) {
$value = $this->config->getAppValue('theming', $key, '');
$output->writeln('- ' . $key . ': ' . $value . '');
}
foreach (self::SUPPORTED_IMAGE_KEYS as $key) {
$value = $this->config->getAppValue('theming', $key . 'Mime', '');
$output->writeln('- ' . $key . ': ' . $value . '');
}
return 0;
}
if (!in_array($key, self::SUPPORTED_KEYS, true)) {
$output->writeln('<error>Invalid config key provided</error>');
return 1;
}
if ($input->getOption('reset')) {
$defaultValue = $this->themingDefaults->undo($key);
$output->writeln('<info>Reset ' . $key . ' to ' . $defaultValue . '</info>');
return 0;
}
if ($value === null) {
$value = $this->config->getAppValue('theming', $key, '');
if ($value !== '') {
$output->writeln('<info>' . $key . ' is currently set to ' . $value . '</info>');
} else {
$output->writeln('<info>' . $key . ' is currently not set</info>');
}
return 0;
}
if (in_array($key, self::SUPPORTED_IMAGE_KEYS, true)) {
if (file_exists(__DIR__ . $value)) {
$value = __DIR__ . $value;
}
if (!file_exists($value)) {
$output->writeln('<error>File could not be found: ' . $value . '</error>');
return 1;
}
$value = $this->imageManager->updateImage($key, $value);
$key = $key . 'Mime';
}
$this->themingDefaults->set($key, $value);
$output->writeln('<info>Updated ' . $key . ' to ' . $value . '</info>');
return 0;
}
}

View File

@@ -214,9 +214,6 @@ class ThemingController extends Controller {
* @throws NotPermittedException
*/
public function uploadImage(): DataResponse {
// logo / background
// new: favicon logo-header
//
$key = $this->request->getParam('key');
$image = $this->request->getUploadedFile('image');
$error = null;
@@ -249,23 +246,14 @@ class ThemingController extends Controller {
);
}
$name = '';
try {
$folder = $this->appData->getFolder('images');
} catch (NotFoundException $e) {
$folder = $this->appData->newFolder('images');
}
$this->imageManager->delete($key);
$target = $folder->newFile($key);
$supportedFormats = $this->getSupportedUploadImageFormats($key);
$detectedMimeType = mime_content_type($image['tmp_name']);
if (!in_array($image['type'], $supportedFormats) || !in_array($detectedMimeType, $supportedFormats)) {
$mime = $this->imageManager->updateImage($key, $image['tmp_name']);
$this->themingDefaults->set($key . 'Mime', $mime);
} catch (\Exception $e) {
return new DataResponse(
[
'data' => [
'message' => $this->l10n->t('Unsupported image type'),
'message' => $e->getMessage()
],
'status' => 'failure',
],
@@ -273,28 +261,7 @@ class ThemingController extends Controller {
);
}
if ($key === 'background' && strpos($detectedMimeType, 'image/svg') === false) {
// Optimize the image since some people may upload images that will be
// either to big or are not progressive rendering.
$newImage = @imagecreatefromstring(file_get_contents($image['tmp_name'], 'r'));
$tmpFile = $this->tempManager->getTemporaryFile();
$newWidth = imagesx($newImage) < 4096 ? imagesx($newImage) : 4096;
$newHeight = imagesy($newImage) / (imagesx($newImage) / $newWidth);
$outputImage = imagescale($newImage, $newWidth, $newHeight);
imageinterlace($outputImage, 1);
imagejpeg($outputImage, $tmpFile, 75);
imagedestroy($outputImage);
$target->putContent(file_get_contents($tmpFile, 'r'));
} else {
$target->putContent(file_get_contents($image['tmp_name'], 'r'));
}
$name = $image['name'];
$this->themingDefaults->set($key.'Mime', $image['type']);
$cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core');
return new DataResponse(
@@ -311,24 +278,6 @@ class ThemingController extends Controller {
);
}
/**
* Returns a list of supported mime types for image uploads.
* "favicon" images are only allowed to be SVG when imagemagick with SVG support is available.
*
* @param string $key The image key, e.g. "favicon"
* @return array
*/
private function getSupportedUploadImageFormats(string $key): array {
$supportedFormats = ['image/jpeg', 'image/png', 'image/gif',];
if ($key !== 'favicon' || $this->imageManager->shouldReplaceIcons() === true) {
$supportedFormats[] = 'image/svg+xml';
$supportedFormats[] = 'image/svg';
}
return $supportedFormats;
}
/**
* Revert setting to default value
*
@@ -341,11 +290,6 @@ class ThemingController extends Controller {
// reprocess server scss for preview
$cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core');
if (strpos($setting, 'Mime') !== -1) {
$imageKey = str_replace('Mime', '', $setting);
$this->imageManager->delete($imageKey);
}
return new DataResponse(
[
'data' =>

View File

@@ -36,6 +36,7 @@ use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\ILogger;
use OCP\ITempManager;
use OCP\IURLGenerator;
class ImageManager {
@@ -52,27 +53,22 @@ class ImageManager {
private $cacheFactory;
/** @var ILogger */
private $logger;
/** @var ITempManager */
private $tempManager;
/**
* ImageManager constructor.
*
* @param IConfig $config
* @param IAppData $appData
* @param IURLGenerator $urlGenerator
* @param ICacheFactory $cacheFactory
* @param ILogger $logger
*/
public function __construct(IConfig $config,
IAppData $appData,
IURLGenerator $urlGenerator,
ICacheFactory $cacheFactory,
ILogger $logger
ILogger $logger,
ITempManager $tempManager
) {
$this->config = $config;
$this->appData = $appData;
$this->urlGenerator = $urlGenerator;
$this->cacheFactory = $cacheFactory;
$this->logger = $logger;
$this->tempManager = $tempManager;
}
public function getImageUrl(string $key, bool $useSvg = true): string {
@@ -211,6 +207,62 @@ class ImageManager {
}
}
public function updateImage(string $key, string $tmpFile) {
$this->delete($key);
try {
$folder = $this->appData->getFolder('images');
} catch (NotFoundException $e) {
$folder = $this->appData->newFolder('images');
}
$target = $folder->newFile($key);
$supportedFormats = $this->getSupportedUploadImageFormats($key);
$detectedMimeType = mime_content_type($tmpFile);
if (!in_array($detectedMimeType, $supportedFormats, true)) {
throw new \Exception('Unsupported image type');
}
if ($key === 'background' && strpos($detectedMimeType, 'image/svg') === false) {
// Optimize the image since some people may upload images that will be
// either to big or are not progressive rendering.
$newImage = @imagecreatefromstring(file_get_contents($tmpFile));
$tmpFile = $this->tempManager->getTemporaryFile();
$newWidth = (int)(imagesx($newImage) < 4096 ? imagesx($newImage) : 4096);
$newHeight = (int)(imagesy($newImage) / (imagesx($newImage) / $newWidth));
$outputImage = imagescale($newImage, $newWidth, $newHeight);
imageinterlace($outputImage, 1);
imagejpeg($outputImage, $tmpFile, 75);
imagedestroy($outputImage);
$target->putContent(file_get_contents($tmpFile));
} else {
$target->putContent(file_get_contents($tmpFile));
}
return $detectedMimeType;
}
/**
* Returns a list of supported mime types for image uploads.
* "favicon" images are only allowed to be SVG when imagemagick with SVG support is available.
*
* @param string $key The image key, e.g. "favicon"
* @return array
*/
private function getSupportedUploadImageFormats(string $key): array {
$supportedFormats = ['image/jpeg', 'image/png', 'image/gif'];
if ($key !== 'favicon' || $this->shouldReplaceIcons() === true) {
$supportedFormats[] = 'image/svg+xml';
$supportedFormats[] = 'image/svg';
}
return $supportedFormats;
}
/**
* remove cached files that are not required any longer
*

View File

@@ -398,6 +398,7 @@ class ThemingDefaults extends \OC_Defaults {
$this->config->deleteAppValue('theming', $setting);
$this->increaseCacheBuster();
$returnValue = '';
switch ($setting) {
case 'name':
$returnValue = $this->getEntity();
@@ -411,8 +412,11 @@ class ThemingDefaults extends \OC_Defaults {
case 'color':
$returnValue = $this->getColorPrimary();
break;
default:
$returnValue = '';
case 'logo':
case 'logoheader':
case 'background':
case 'favicon':
$this->imageManager->delete($setting);
break;
}

View File

@@ -47,34 +47,32 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ITempManager;
use OCP\IURLGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class ThemingControllerTest extends TestCase {
/** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
/** @var IRequest|MockObject */
private $request;
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
/** @var IConfig|MockObject */
private $config;
/** @var ThemingDefaults|\PHPUnit\Framework\MockObject\MockObject */
/** @var ThemingDefaults|MockObject */
private $themingDefaults;
/** @var \OCP\AppFramework\Utility\ITimeFactory */
private $timeFactory;
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
/** @var IL10N|MockObject */
private $l10n;
/** @var ThemingController */
private $themingController;
/** @var ITempManager */
private $tempManager;
/** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
/** @var IAppManager|MockObject */
private $appManager;
/** @var IAppData|\PHPUnit\Framework\MockObject\MockObject */
/** @var IAppData|MockObject */
private $appData;
/** @var ImageManager|\PHPUnit\Framework\MockObject\MockObject */
/** @var ImageManager|MockObject */
private $imageManager;
/** @var SCSSCacher */
private $scssCacher;
@@ -93,12 +91,12 @@ class ThemingControllerTest extends TestCase {
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->imageManager = $this->createMock(ImageManager::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->timeFactory->expects($this->any())
$timeFactory = $this->createMock(ITimeFactory::class);
$timeFactory->expects($this->any())
->method('getTime')
->willReturn(123);
$this->overwriteService(ITimeFactory::class, $this->timeFactory);
$this->overwriteService(ITimeFactory::class, $timeFactory);
$this->themingController = new ThemingController(
'theming',
@@ -287,12 +285,9 @@ class ThemingControllerTest extends TestCase {
return $str;
});
$folder = $this->createMock(ISimpleFolder::class);
$this->appData
->expects($this->once())
->method('getFolder')
->with('images')
->willReturn($folder);
$this->imageManager->expects($this->once())
->method('updateImage')
->willThrowException(new \Exception('Unsupported image type'));
$expected = new DataResponse(
[
@@ -331,12 +326,9 @@ class ThemingControllerTest extends TestCase {
return $str;
});
$folder = $this->createMock(ISimpleFolder::class);
$this->appData
->expects($this->once())
->method('getFolder')
->with('images')
->willReturn($folder);
$this->imageManager->expects($this->once())
->method('updateImage')
->willThrowException(new \Exception('Unsupported image type'));
$expected = new DataResponse(
[
@@ -392,31 +384,6 @@ class ThemingControllerTest extends TestCase {
return $str;
});
$file = $this->createMock(ISimpleFile::class);
$folder = $this->createMock(ISimpleFolder::class);
if ($folderExists) {
$this->appData
->expects($this->once())
->method('getFolder')
->with('images')
->willReturn($folder);
} else {
$this->appData
->expects($this->at(0))
->method('getFolder')
->with('images')
->willThrowException(new NotFoundException());
$this->appData
->expects($this->at(1))
->method('newFolder')
->with('images')
->willReturn($folder);
}
$folder->expects($this->once())
->method('newFile')
->with('logo')
->willReturn($file);
$this->urlGenerator->expects($this->once())
->method('linkTo')
->willReturn('serverCss');
@@ -424,6 +391,10 @@ class ThemingControllerTest extends TestCase {
->method('getImageUrl')
->with('logo')
->willReturn('imageUrl');
$this->imageManager->expects($this->once())
->method('updateImage');
$expected = new DataResponse(
[
'data' =>
@@ -468,30 +439,8 @@ class ThemingControllerTest extends TestCase {
return $str;
});
$file = $this->createMock(ISimpleFile::class);
$folder = $this->createMock(ISimpleFolder::class);
if ($folderExists) {
$this->appData
->expects($this->once())
->method('getFolder')
->with('images')
->willReturn($folder);
} else {
$this->appData
->expects($this->at(0))
->method('getFolder')
->with('images')
->willThrowException(new NotFoundException());
$this->appData
->expects($this->at(1))
->method('newFolder')
->with('images')
->willReturn($folder);
}
$folder->expects($this->once())
->method('newFile')
->with('background')
->willReturn($file);
$this->imageManager->expects($this->once())
->method('updateImage');
$this->urlGenerator->expects($this->once())
->method('linkTo')
@@ -542,12 +491,9 @@ class ThemingControllerTest extends TestCase {
return $str;
});
$folder = $this->createMock(ISimpleFolder::class);
$this->appData
->expects($this->once())
->method('getFolder')
->with('images')
->willReturn($folder);
$this->imageManager->expects($this->once())
->method('updateImage')
->willThrowException(new \Exception('Unsupported image type'));
$expected = new DataResponse(
[
@@ -717,9 +663,6 @@ class ThemingControllerTest extends TestCase {
->method('linkTo')
->with('', '/core/css/someHash-css-variables.scss')
->willReturn('/nextcloudWebroot/core/css/someHash-css-variables.scss');
$this->imageManager->expects($this->once())
->method('delete')
->with($filename);
$expected = new DataResponse(
[

View File

@@ -36,23 +36,27 @@ use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\ILogger;
use OCP\ITempManager;
use OCP\IURLGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class ImageManagerTest extends TestCase {
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
/** @var IConfig|MockObject */
protected $config;
/** @var IAppData|\PHPUnit\Framework\MockObject\MockObject */
/** @var IAppData|MockObject */
protected $appData;
/** @var ImageManager */
protected $imageManager;
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
/** @var IURLGenerator|MockObject */
private $urlGenerator;
/** @var ICacheFactory|\PHPUnit\Framework\MockObject\MockObject */
/** @var ICacheFactory|MockObject */
private $cacheFactory;
/** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */
/** @var ILogger|MockObject */
private $logger;
/** @var ITempManager|MockObject */
private $tempManager;
protected function setUp(): void {
parent::setUp();
@@ -61,12 +65,14 @@ class ImageManagerTest extends TestCase {
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->cacheFactory = $this->createMock(ICacheFactory::class);
$this->logger = $this->createMock(ILogger::class);
$this->tempManager = $this->createMock(ITempManager::class);
$this->imageManager = new ImageManager(
$this->config,
$this->appData,
$this->urlGenerator,
$this->cacheFactory,
$this->logger
$this->logger,
$this->tempManager
);
}
@@ -84,7 +90,7 @@ class ImageManagerTest extends TestCase {
}
public function mockGetImage($key, $file) {
/** @var \PHPUnit\Framework\MockObject\MockObject $folder */
/** @var MockObject $folder */
$folder = $this->createMock(ISimpleFolder::class);
if ($file === null) {
$folder->expects($this->once())
@@ -327,4 +333,56 @@ class ImageManagerTest extends TestCase {
->willReturn($folders[2]);
$this->imageManager->cleanup();
}
public function dataUpdateImage() {
return [
['background', __DIR__ . '/../../../tests/data/testimage.png', true, true],
['background', __DIR__ . '/../../../tests/data/testimage.png', false, true],
['background', __DIR__ . '/../../../tests/data/testimage.jpg', true, true],
['logo', __DIR__ . '/../../../tests/data/testimagelarge.svg', true, false],
];
}
/**
* @dataProvider dataUpdateImage
*/
public function testUpdateImage($key, $tmpFile, $folderExists, $shouldConvert) {
$file = $this->createMock(ISimpleFile::class);
$folder = $this->createMock(ISimpleFolder::class);
$oldFile = $this->createMock(ISimpleFile::class);
$folder->expects($this->any())
->method('getFile')
->willReturn($oldFile);
if ($folderExists) {
$this->appData
->expects($this->any())
->method('getFolder')
->with('images')
->willReturn($folder);
} else {
$this->appData
->expects($this->any())
->method('getFolder')
->with('images')
->willThrowException(new NotFoundException());
$this->appData
->expects($this->any())
->method('newFolder')
->with('images')
->willReturn($folder);
}
$folder->expects($this->once())
->method('newFile')
->with($key)
->willReturn($file);
if ($shouldConvert) {
$this->tempManager->expects($this->once())
->method('getTemporaryFile')
->willReturn('/tmp/randomtempfile-theming');
}
$this->imageManager->updateImage($key, $tmpFile);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

View File

@@ -138,7 +138,12 @@ class UserStatusController extends OCSController {
string $message,
?int $clearAt): DataResponse {
try {
$status = $this->service->setCustomMessage($this->userId, $statusIcon, $message, $clearAt);
if ($message !== '') {
$status = $this->service->setCustomMessage($this->userId, $statusIcon, $message, $clearAt);
} else {
$this->service->clearMessage($this->userId);
$status = $this->service->findByUserId($this->userId);
}
return new DataResponse($this->formatStatus($status));
} catch (InvalidClearAtException $ex) {
$this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid clearAt value "' . $clearAt . '"');

View File

@@ -77,7 +77,7 @@ class StatusService {
];
/** @var int */
public const INVALIDATE_STATUS_THRESHOLD = 5 /* minutes */ * 60 /* seconds */;
public const INVALIDATE_STATUS_THRESHOLD = 15 /* minutes */ * 60 /* seconds */;
/** @var int */
public const MAXIMUM_MESSAGE_LENGTH = 80;

View File

@@ -20,70 +20,59 @@
-->
<template>
<li :class="{ inline }">
<div id="user-status-menu-item">
<li>
<div class="user-status-menu-item">
<!-- Username display -->
<span
v-if="!inline"
id="user-status-menu-item__header"
class="user-status-menu-item__header"
:title="displayName">
{{ displayName }}
</span>
<Actions
id="user-status-menu-item__subheader"
:default-icon="statusIcon"
container="header"
:menu-title="visibleMessage"
:title="visibleMessage">
<ActionButton
v-for="status in statuses"
:key="status.type"
:icon="status.icon"
:close-after-click="true"
:title="status.label"
@click.prevent.stop="changeStatus(status.type)">
{{ status.subline }}
</ActionButton>
<ActionButton
icon="icon-rename"
:close-after-click="true"
:title="$t('user_status', 'Set custom status')"
@click.prevent.stop="openModal" />
</Actions>
<SetStatusModal
v-if="isModalOpen"
@close="closeModal" />
<!-- Status modal toggle -->
<toggle :is="inline ? 'button' : 'a'"
:class="{'user-status-menu-item__toggle--inline': inline}"
class="user-status-menu-item__toggle"
href="#"
@click.prevent.stop="openModal">
<span :class="statusIcon" class="user-status-menu-item__toggle-icon" />
{{ visibleMessage }}
</toggle>
</div>
<!-- Status management modal -->
<SetStatusModal
v-if="isModalOpen"
@close="closeModal" />
</li>
</template>
<script>
import { getCurrentUser } from '@nextcloud/auth'
import SetStatusModal from './components/SetStatusModal'
import Actions from '@nextcloud/vue/dist/Components/Actions'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import { mapState } from 'vuex'
import { showError } from '@nextcloud/dialogs'
import { getAllStatusOptions } from './services/statusOptionsService'
import { sendHeartbeat } from './services/heartbeatService'
import debounce from 'debounce'
import { sendHeartbeat } from './services/heartbeatService'
import OnlineStatusMixin from './mixins/OnlineStatusMixin'
export default {
name: 'App',
name: 'UserStatus',
components: {
Actions,
ActionButton,
SetStatusModal,
SetStatusModal: () => import(/* webpackChunkName: 'user-status-modal' */'./components/SetStatusModal'),
},
mixins: [OnlineStatusMixin],
props: {
inline: {
type: Boolean,
default: false,
},
},
data() {
return {
isModalOpen: false,
statuses: getAllStatusOptions(),
heartbeatInterval: null,
setAwayTimeout: null,
mouseMoveListener: null,
@@ -91,12 +80,6 @@ export default {
}
},
computed: {
...mapState({
statusType: state => state.userStatus.status,
statusIsUserDefined: state => state.userStatus.statusIsUserDefined,
customIcon: state => state.userStatus.icon,
customMessage: state => state.userStatus.message,
}),
/**
* The display-name of the current user
*
@@ -105,64 +88,8 @@ export default {
displayName() {
return getCurrentUser().displayName
},
/**
* The message displayed in the top right corner
*
* @returns {String}
*/
visibleMessage() {
if (this.customIcon && this.customMessage) {
return `${this.customIcon} ${this.customMessage}`
}
if (this.customMessage) {
return this.customMessage
}
if (this.statusIsUserDefined) {
switch (this.statusType) {
case 'online':
return this.$t('user_status', 'Online')
case 'away':
return this.$t('user_status', 'Away')
case 'dnd':
return this.$t('user_status', 'Do not disturb')
case 'invisible':
return this.$t('user_status', 'Invisible')
case 'offline':
return this.$t('user_status', 'Offline')
}
}
return this.$t('user_status', 'Set status')
},
/**
* The status indicator icon
*
* @returns {String|null}
*/
statusIcon() {
switch (this.statusType) {
case 'online':
return 'icon-user-status-online'
case 'away':
return 'icon-user-status-away'
case 'dnd':
return 'icon-user-status-dnd'
case 'invisible':
case 'offline':
return 'icon-user-status-invisible'
}
return ''
},
},
/**
* Loads the current user's status from initial state
* and stores it in Vuex
@@ -198,6 +125,7 @@ export default {
this._backgroundHeartbeat()
}
},
/**
* Some housekeeping before destroying the component
*/
@@ -205,6 +133,7 @@ export default {
window.removeEventListener('mouseMove', this.mouseMoveListener)
clearInterval(this.heartbeatInterval)
},
methods: {
/**
* Opens the modal to set a custom status
@@ -218,19 +147,7 @@ export default {
closeModal() {
this.isModalOpen = false
},
/**
* Changes the user-status
*
* @param {String} statusType (online / away / dnd / invisible)
*/
async changeStatus(statusType) {
try {
await this.$store.dispatch('setStatus', { statusType })
} catch (err) {
showError(this.$t('user_status', 'There was an error saving the new status'))
console.debug(err)
}
},
/**
* Sends the status heartbeat to the server
*
@@ -248,65 +165,55 @@ export default {
<style lang="scss">
$max-width-user-status: 200px;
li:not(.inline) #user-status-menu-item {
.user-status-menu-item {
&__header {
display: block;
box-sizing: border-box;
color: var(--color-text-maxcontrast);
padding: 10px 12px 5px 38px;
opacity: 1;
white-space: nowrap;
text-align: left;
max-width: $max-width-user-status;
overflow: hidden;
box-sizing: border-box;
max-width: $max-width-user-status;
padding: 10px 12px 5px 38px;
text-align: left;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 1;
color: var(--color-text-maxcontrast);
}
&__subheader {
width: 100%;
button.action-item__menutoggle {
display: block;
box-sizing: border-box;
background-color: var(--color-main-background);
background-position: 12px center;
&__toggle {
&-icon {
width: 16px;
height: 16px;
margin-right: 10px;
opacity: 1 !important;
background-size: 16px;
border: 0;
border-radius: 0;
font-weight: normal;
padding-left: 38px;
opacity: 1;
max-width: $max-width-user-status;
overflow: hidden;
text-overflow: ellipsis;
}
// In dashboard
&--inline {
width: auto;
min-width: 44px;
height: 44px;
margin: 0;
border: 0;
border-radius: var(--border-radius-pill);
background-color: var(--color-background-translucent);
font-size: inherit;
font-weight: normal;
-webkit-backdrop-filter: var(--background-blur);
backdrop-filter: var(--background-blur);
&:active,
&:hover,
&:focus {
box-shadow: inset 4px 0 var(--color-primary-element);
background-color: var(--color-background-hover);
}
}
}
}
.inline #user-status-menu-item__subheader {
width: 100%;
button.action-item__menutoggle {
background-size: 16px;
border: 0;
border-radius: var(--border-radius-pill);
font-weight: normal;
padding-left: 40px;
&.icon-loading-small {
&::after {
left: 21px;
}
}
}
li {
list-style-type: none;
}
li {
list-style-type: none;
}
</style>

View File

@@ -0,0 +1,122 @@
<!--
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div class="user-status-online-select">
<input :id="id"
:checked="checked"
class="user-status-online-select__input"
type="radio"
name="user-status-online"
@change="onChange">
<label :for="id" :class="icon" class="user-status-online-select__label">
{{ label }}
<em class="user-status-online-select__subline">{{ subline }}</em>
</label>
</div>
</template>
<script>
export default {
name: 'OnlineStatusSelect',
props: {
checked: {
type: Boolean,
default: false,
},
icon: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
subline: {
type: String,
default: null,
},
},
computed: {
id() {
return `user-status-online-status-${this.type}`
},
},
methods: {
onChange() {
this.$emit('select', this.type)
},
},
}
</script>
<style lang="scss" scoped>
$icon-size: 24px;
$label-padding: 8px;
.user-status-online-select {
// Inputs are here for keyboard navigation, they are not visually visible
&__input {
position: absolute;
top: auto;
left: -10000px;
overflow: hidden;
width: 1px;
height: 1px;
}
&__label {
display: block;
margin: $label-padding;
padding: $label-padding;
padding-left: $icon-size + $label-padding * 2;
border: 2px solid var(--color-main-background);
border-radius: var(--border-radius-large);
background-color: var(--color-background-hover);
background-position: $label-padding center;
background-size: $icon-size;
span,
& {
cursor: pointer;
}
}
&__input:checked + &__label,
&__input:focus + &__label,
&__label:hover {
border-color: var(--color-primary);
}
&__subline {
display: block;
color: var(--color-text-lighter);
}
}
</style>

View File

@@ -22,11 +22,24 @@
<template>
<Modal
size="normal"
:title="$t('user_status', 'Set a custom status')"
:title="$t('user_status', 'Set status')"
@close="closeModal">
<div class="set-status-modal">
<!-- Status selector -->
<div class="set-status-modal__header">
<h3>{{ $t('user_status', 'Set a custom status') }}</h3>
<h3>{{ $t('user_status', 'Online status') }}</h3>
</div>
<div class="set-status-modal__online-status">
<OnlineStatusSelect v-for="status in statuses"
:key="status.type"
v-bind="status"
:checked="status.type === statusType"
@select="changeStatus" />
</div>
<!-- Status message -->
<div class="set-status-modal__header">
<h3>{{ $t('user_status', 'Status message') }}</h3>
</div>
<div class="set-status-modal__custom-input">
<EmojiPicker @select="setIcon">
@@ -46,10 +59,10 @@
@selectClearAt="setClearAt" />
<div class="status-buttons">
<button class="status-buttons__select" @click="clearStatus">
{{ $t('user_status', 'Clear custom status') }}
{{ $t('user_status', 'Clear status message') }}
</button>
<button class="status-buttons__primary primary" @click="saveStatus">
{{ $t('user_status', 'Set status') }}
{{ $t('user_status', 'Set status message') }}
</button>
</div>
</div>
@@ -57,27 +70,36 @@
</template>
<script>
import { showError } from '@nextcloud/dialogs'
import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker'
import Modal from '@nextcloud/vue/dist/Components/Modal'
import { getAllStatusOptions } from '../services/statusOptionsService'
import OnlineStatusMixin from '../mixins/OnlineStatusMixin'
import PredefinedStatusesList from './PredefinedStatusesList'
import CustomMessageInput from './CustomMessageInput'
import ClearAtSelect from './ClearAtSelect'
import { showError } from '@nextcloud/dialogs'
import OnlineStatusSelect from './OnlineStatusSelect'
export default {
name: 'SetStatusModal',
components: {
ClearAtSelect,
CustomMessageInput,
EmojiPicker,
Modal,
CustomMessageInput,
OnlineStatusSelect,
PredefinedStatusesList,
ClearAtSelect,
},
mixins: [OnlineStatusMixin],
data() {
return {
clearAt: null,
icon: null,
message: null,
clearAt: null,
statuses: getAllStatusOptions(),
}
},
computed: {
@@ -90,6 +112,7 @@ export default {
return this.icon || '😀'
},
},
/**
* Loads the current status when a user opens dialog
*/
@@ -208,6 +231,21 @@ export default {
min-width: 500px;
min-height: 200px;
padding: 8px 20px 20px 20px;
// Enable scrollbar for too long content, same way as in Dashboard customize
max-height: 70vh;
overflow: auto;
&__header {
text-align: center;
font-weight: bold;
}
&__online-status {
display: grid;
// Space between the two sections
margin-bottom: 40px;
grid-template-columns: 1fr 1fr;
}
&__custom-input {
display: flex;
@@ -216,12 +254,12 @@ export default {
.custom-input__emoji-button {
flex-basis: 40px;
width: 40px;
flex-grow: 0;
border-radius: var(--border-radius) 0 0 var(--border-radius);
width: 40px;
height: 34px;
margin-right: 0;
border-right: none;
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
}
@@ -233,4 +271,5 @@ export default {
}
}
}
</style>

View File

@@ -21,7 +21,7 @@
*/
import Vue from 'vue'
import { getRequestToken } from '@nextcloud/auth'
import App from './App'
import UserStatus from './UserStatus'
import store from './store'
// eslint-disable-next-line camelcase
@@ -36,18 +36,23 @@ __webpack_public_path__ = OC.linkTo('user_status', 'js/')
Vue.prototype.t = t
Vue.prototype.$t = t
const app = new Vue({
render: h => h(App),
// Register settings menu entry
export default new Vue({
el: 'li[data-id="user_status-menuitem"]',
// eslint-disable-next-line vue/match-component-file-name
name: 'UserStatusRoot',
render: h => h(UserStatus),
store,
}).$mount('li[data-id="user_status-menuitem"]')
})
// Register dashboard status
document.addEventListener('DOMContentLoaded', function() {
if (!OCA.Dashboard) {
return
}
OCA.Dashboard.registerStatus('status', (el) => {
const Dashboard = Vue.extend(App)
const Dashboard = Vue.extend(UserStatus)
return new Dashboard({
propsData: {
inline: true,
@@ -56,5 +61,3 @@ document.addEventListener('DOMContentLoaded', function() {
}).$mount(el)
})
})
export { app }

View File

@@ -0,0 +1,110 @@
/**
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { mapState } from 'vuex'
import { showError } from '@nextcloud/dialogs'
export default {
computed: {
...mapState({
statusType: state => state.userStatus.status,
statusIsUserDefined: state => state.userStatus.statusIsUserDefined,
customIcon: state => state.userStatus.icon,
customMessage: state => state.userStatus.message,
}),
/**
* The message displayed in the top right corner
*
* @returns {String}
*/
visibleMessage() {
if (this.customIcon && this.customMessage) {
return `${this.customIcon} ${this.customMessage}`
}
if (this.customMessage) {
return this.customMessage
}
if (this.statusIsUserDefined) {
switch (this.statusType) {
case 'online':
return this.$t('user_status', 'Online')
case 'away':
return this.$t('user_status', 'Away')
case 'dnd':
return this.$t('user_status', 'Do not disturb')
case 'invisible':
return this.$t('user_status', 'Invisible')
case 'offline':
return this.$t('user_status', 'Offline')
}
}
return this.$t('user_status', 'Set status')
},
/**
* The status indicator icon
*
* @returns {String|null}
*/
statusIcon() {
switch (this.statusType) {
case 'online':
return 'icon-user-status-online'
case 'away':
return 'icon-user-status-away'
case 'dnd':
return 'icon-user-status-dnd'
case 'invisible':
case 'offline':
return 'icon-user-status-invisible'
}
return ''
},
},
methods: {
/**
* Changes the user-status
*
* @param {String} statusType (online / away / dnd / invisible)
*/
async changeStatus(statusType) {
try {
await this.$store.dispatch('setStatus', { statusType })
} catch (err) {
showError(this.$t('user_status', 'There was an error saving the new status'))
console.debug(err)
}
},
},
}

View File

@@ -44,6 +44,7 @@ const getAllStatusOptions = () => {
}, {
type: 'invisible',
label: t('user_status', 'Invisible'),
subline: t('user_status', 'Appear offline'),
icon: 'icon-user-status-invisible',
}]
}

View File

@@ -24,6 +24,20 @@
id="user-status_panel"
:items="items"
:loading="loading">
<template v-slot:default="{ item }">
<DashboardWidgetItem
:main-text="item.mainText"
:sub-text="item.subText">
<template v-slot:avatar>
<Avatar
class="item-avatar"
:size="44"
:user="item.avatarUsername"
:display-name="item.mainText"
:show-user-status-compact="false" />
</template>
</DashboardWidgetItem>
</template>
<template v-slot:empty-content>
<EmptyContent
id="user_status-widget-empty-content"
@@ -35,7 +49,8 @@
</template>
<script>
import { DashboardWidget } from '@nextcloud/vue-dashboard'
import { DashboardWidget, DashboardWidgetItem } from '@nextcloud/vue-dashboard'
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
import { loadState } from '@nextcloud/initial-state'
import moment from '@nextcloud/moment'
@@ -43,7 +58,9 @@ import moment from '@nextcloud/moment'
export default {
name: 'Dashboard',
components: {
Avatar,
DashboardWidget,
DashboardWidgetItem,
EmptyContent,
},
data() {
@@ -56,13 +73,21 @@ export default {
items() {
return this.statuses.map((item) => {
const icon = item.icon || ''
const message = item.message || ''
const status = `${icon} ${message}`
let message = item.message || ''
if (message === '') {
if (item.status === 'away') {
message = t('user_status', 'Away')
}
if (item.status === 'dnd') {
message = t('user_status', 'Do not disturb')
}
}
const status = item.icon !== '' ? `${icon} ${message}` : message
let subText
if (item.icon === null && item.message === null && item.timestamp === null) {
if (item.icon === null && message === '' && item.timestamp === null) {
subText = ''
} else if (item.icon === null && item.message === null && item.timestamp !== null) {
} else if (item.icon === null && message === '' && item.timestamp !== null) {
subText = moment(item.timestamp, 'X').fromNow()
} else if (item.timestamp !== null) {
subText = this.t('user_status', '{status}, {timestamp}', {

View File

@@ -56,7 +56,7 @@ class ClearOldStatusesBackgroundJobTest extends TestCase {
->with(1337);
$this->mapper->expects($this->once())
->method('clearStatusesOlderThan')
->with(1037, 1337);
->with(437, 1337);
$this->time->method('getTime')
->willReturn(1337);

View File

@@ -243,6 +243,7 @@ class UserStatusControllerTest extends TestCase {
* @param Throwable|null $exception
* @param bool $expectLogger
* @param string|null $expectedLogMessage
* @param bool $expectSuccessAsReset
*
* @dataProvider setCustomMessageDataProvider
*/
@@ -253,7 +254,8 @@ class UserStatusControllerTest extends TestCase {
bool $expectException,
?Throwable $exception,
bool $expectLogger,
?string $expectedLogMessage): void {
?string $expectedLogMessage,
bool $expectSuccessAsReset = false): void {
$userStatus = $this->getUserStatus();
if ($expectException) {
@@ -262,10 +264,25 @@ class UserStatusControllerTest extends TestCase {
->with('john.doe', $statusIcon, $message, $clearAt)
->willThrowException($exception);
} else {
$this->service->expects($this->once())
->method('setCustomMessage')
->with('john.doe', $statusIcon, $message, $clearAt)
->willReturn($userStatus);
if ($expectSuccessAsReset) {
$this->service->expects($this->never())
->method('setCustomMessage');
$this->service->expects($this->once())
->method('clearMessage')
->with('john.doe');
$this->service->expects($this->once())
->method('findByUserId')
->with('john.doe')
->willReturn($userStatus);
} else {
$this->service->expects($this->once())
->method('setCustomMessage')
->with('john.doe', $statusIcon, $message, $clearAt)
->willReturn($userStatus);
$this->service->expects($this->never())
->method('clearMessage');
}
}
if ($expectLogger) {
@@ -297,6 +314,7 @@ class UserStatusControllerTest extends TestCase {
public function setCustomMessageDataProvider(): array {
return [
['👨🏽‍💻', 'Busy developing the status feature', 500, true, false, null, false, null],
['👨🏽‍💻', '', 500, true, false, null, false, null, true],
['👨🏽‍💻', 'Busy developing the status feature', 500, false, true, new InvalidClearAtException('Original exception message'), true,
'New user-status for "john.doe" was rejected due to an invalid clearAt value "500"'],
['👨🏽‍💻', 'Busy developing the status feature', 500, false, true, new InvalidStatusIconException('Original exception message'), true,

View File

@@ -152,7 +152,7 @@ class StatusServiceTest extends TestCase {
$status->setIsUserDefined(true);
$this->timeFactory->method('getTime')
->willReturn(1400);
->willReturn(2600);
$this->mapper->expects($this->once())
->method('findByUserId')
->with('john.doe')
@@ -160,7 +160,7 @@ class StatusServiceTest extends TestCase {
$this->assertEquals($status, $this->service->findByUserId('john.doe'));
$this->assertEquals('offline', $status->getStatus());
$this->assertEquals(1400, $status->getStatusTimestamp());
$this->assertEquals(2600, $status->getStatusTimestamp());
$this->assertFalse($status->getIsUserDefined());
}

View File

@@ -2,18 +2,18 @@ const path = require('path')
module.exports = {
entry: {
'dashboard': path.join(__dirname, 'src', 'dashboard'),
'user-status-menu': path.join(__dirname, 'src', 'main-user-status-menu')
dashboard: path.join(__dirname, 'src', 'dashboard'),
'user-status-menu': path.join(__dirname, 'src', 'main-user-status-menu'),
},
output: {
path: path.resolve(__dirname, './js'),
publicPath: '/js/',
filename: '[name].js?v=[chunkhash]',
jsonpFunction: 'webpackJsonpUserStatus'
jsonpFunction: 'webpackJsonpUserStatus',
},
optimization: {
splitChunks: {
automaticNameDelimiter: '-',
}
}
},
},
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

View File

@@ -46,7 +46,7 @@ class WeatherStatusController extends OCSController {
IRequest $request,
ILogger $logger,
WeatherStatusService $service,
string $userId) {
?string $userId) {
parent::__construct($appName, $request);
$this->userId = $userId;
$this->logger = $logger;

View File

@@ -105,7 +105,7 @@ class WeatherStatusService {
IUserManager $userManager,
IAppManager $appManager,
ICacheFactory $cacheFactory,
string $userId) {
?string $userId) {
$this->config = $config;
$this->userId = $userId;
$this->l10n = $l10n;

View File

@@ -240,7 +240,11 @@ export default {
console.info('The weather status request was cancelled because the user navigates.')
return
}
showError(t('weather_status', 'There was an error getting the weather status information.'))
if (err.response && err.response.status === 401) {
showError(t('weather_status', 'You are not logged in.'))
} else {
showError(t('weather_status', 'There was an error getting the weather status information.'))
}
console.error(err)
}
},
@@ -309,8 +313,11 @@ export default {
this.loading = false
}
} catch (err) {
showError(t('weather_status', 'There was an error setting the location address.'))
console.debug(err)
if (err.response && err.response.status === 401) {
showError(t('weather_status', 'You are not logged in.'))
} else {
showError(t('weather_status', 'There was an error setting the location address.'))
}
this.loading = false
}
},
@@ -320,7 +327,11 @@ export default {
this.address = loc.address
this.startLoop()
} catch (err) {
showError(t('weather_status', 'There was an error setting the location.'))
if (err.response && err.response.status === 401) {
showError(t('weather_status', 'You are not logged in.'))
} else {
showError(t('weather_status', 'There was an error setting the location.'))
}
console.debug(err)
}
},
@@ -328,7 +339,11 @@ export default {
try {
await network.setMode(mode)
} catch (err) {
showError(t('weather_status', 'There was an error saving the mode.'))
if (err.response && err.response.status === 401) {
showError(t('weather_status', 'You are not logged in.'))
} else {
showError(t('weather_status', 'There was an error saving the mode.'))
}
console.debug(err)
}
},
@@ -345,7 +360,11 @@ export default {
this.mode = MODE_MANUAL_LOCATION
this.startLoop()
} catch (err) {
showError(t('weather_status', 'There was an error using personal address.'))
if (err.response && err.response.status === 401) {
showError(t('weather_status', 'You are not logged in.'))
} else {
showError(t('weather_status', 'There was an error using personal address.'))
}
console.debug(err)
this.loading = false
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -27,6 +27,7 @@
require __DIR__ . '/../../vendor/autoload.php';
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use PHPUnit\Framework\Assert;
class CommandLineContext implements \Behat\Behat\Context\Context {
use CommandLine;
@@ -129,4 +130,11 @@ class CommandLineContext implements \Behat\Behat\Context\Context {
$davPath = rtrim($davPath, '/') . $this->lastTransferPath;
$this->featureContext->usingDavPath($davPath);
}
/**
* @Then /^transfer folder name contains "([^"]+)"$/
*/
public function transferFolderNameContains($text) {
Assert::assertContains($text, $this->lastTransferPath);
}
}

View File

@@ -70,7 +70,7 @@ trait Provisioning {
}
/**
* @Given /^user "([^"]*)" with displayname "([^"]*)" exists$/
* @Given /^user "([^"]*)" with displayname "((?:[^"]|\\")*)" exists$/
* @param string $user
*/
public function assureUserWithDisplaynameExists($user, $displayname) {

View File

@@ -29,6 +29,22 @@ Feature: transfer-ownership
And using received transfer folder of "user1" as dav path
And as "user1" the folder "/test" exists
Scenario: transferring ownership from user with risky display name
Given user "user0" with displayname "user0 \"risky\"? spay 'na|\/|e':.#" exists
And user "user1" exists
And User "user0" created a folder "/test"
And User "user0" uploads file "data/textfile.txt" to "/test/somefile.txt"
When transferring ownership from "user0" to "user1"
And the command was successful
And As an "user1"
And using received transfer folder of "user1" as dav path
Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is"
And transfer folder name contains "transferred from user0 -risky- spay -na|-|e- on"
And using old dav path
And as "user0" the folder "/test" does not exist
And using received transfer folder of "user1" as dav path
And as "user1" the folder "/test" exists
Scenario: transferring ownership of file shares
Given user "user0" exists
And user "user1" exists
@@ -290,6 +306,20 @@ Feature: transfer-ownership
Then the command error output contains the text "Unknown target user"
And the command failed with exit code 1
Scenario: transferring ownership of a file
Given user "user0" exists
And user "user1" exists
And User "user0" uploads file "data/textfile.txt" to "/somefile.txt"
When transferring ownership of path "somefile.txt" from "user0" to "user1"
And the command was successful
And As an "user1"
And using received transfer folder of "user1" as dav path
Then Downloaded content when downloading file "/somefile.txt" with range "bytes=0-6" should be "This is"
And using old dav path
And as "user0" the file "/somefile.txt" does not exist
And using received transfer folder of "user1" as dav path
And as "user1" the file "/somefile.txt" exists
Scenario: transferring ownership of a folder
Given user "user0" exists
And user "user1" exists
@@ -305,6 +335,73 @@ Feature: transfer-ownership
And using received transfer folder of "user1" as dav path
And as "user1" the folder "/test" exists
Scenario: transferring ownership from user with risky display name
Given user "user0" with displayname "user0 \"risky\"? spay 'na|\/|e':.#" exists
And user "user1" exists
And User "user0" created a folder "/test"
And User "user0" uploads file "data/textfile.txt" to "/test/somefile.txt"
When transferring ownership of path "test" from "user0" to "user1"
And the command was successful
And As an "user1"
And using received transfer folder of "user1" as dav path
Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is"
And transfer folder name contains "transferred from user0 -risky- spay -na|-|e- on"
And using old dav path
And as "user0" the folder "/test" does not exist
And using received transfer folder of "user1" as dav path
And as "user1" the folder "/test" exists
Scenario: transferring ownership of path does not affect other files
Given user "user0" exists
And user "user1" exists
And User "user0" created a folder "/test"
And User "user0" uploads file "data/textfile.txt" to "/test/somefile.txt"
And User "user0" created a folder "/test2"
And User "user0" uploads file "data/textfile.txt" to "/test2/somefile.txt"
When transferring ownership of path "test" from "user0" to "user1"
And the command was successful
And As an "user1"
And using received transfer folder of "user1" as dav path
Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is"
And using old dav path
And as "user0" the folder "/test" does not exist
And as "user0" the folder "/test2" exists
And as "user0" the file "/test2/somefile.txt" exists
And using received transfer folder of "user1" as dav path
And as "user1" the folder "/test" exists
And as "user1" the folder "/test2" does not exist
Scenario: transferring ownership of path does not affect other shares
Given user "user0" exists
And user "user1" exists
And User "user0" created a folder "/test"
And User "user0" uploads file "data/textfile.txt" to "/test/somefile.txt"
And User "user0" created a folder "/test2"
And User "user0" uploads file "data/textfile.txt" to "/test2/sharedfile.txt"
And file "/test2/sharedfile.txt" of user "user0" is shared with user "user1" with permissions 19
And user "user1" accepts last share
When transferring ownership of path "test" from "user0" to "user1"
And the command was successful
And As an "user1"
And using received transfer folder of "user1" as dav path
Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is"
And using old dav path
And as "user0" the folder "/test" does not exist
And as "user0" the folder "/test2" exists
And as "user0" the file "/test2/sharedfile.txt" exists
And using received transfer folder of "user1" as dav path
And as "user1" the folder "/test" exists
And as "user1" the folder "/test2" does not exist
And using old dav path
And as "user1" the file "/sharedfile.txt" exists
And As an "user1"
And Getting info of last share
And the OCS status code should be "100"
And Share fields of last share match with
| uid_owner | user0 |
| uid_file_owner | user0 |
| share_with | user1 |
Scenario: transferring ownership of file shares
Given user "user0" exists
And user "user1" exists

View File

@@ -33,6 +33,8 @@ use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\ILogger;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
@@ -54,11 +56,14 @@ class Repair extends Command {
private $memoryLimit;
/** @var int */
private $memoryTreshold;
/** @var ILockingProvider */
private $lockingProvider;
public function __construct(IConfig $config, IRootFolder $rootFolder, ILogger $logger, IniGetWrapper $phpIni) {
public function __construct(IConfig $config, IRootFolder $rootFolder, ILogger $logger, IniGetWrapper $phpIni, ILockingProvider $lockingProvider) {
$this->config = $config;
$this->rootFolder = $rootFolder;
$this->logger = $logger;
$this->lockingProvider = $lockingProvider;
$this->memoryLimit = $phpIni->getBytes('memory_limit');
$this->memoryTreshold = $this->memoryLimit - 25 * 1024 * 1024;
@@ -95,8 +100,6 @@ class Repair extends Command {
$output->writeln("");
}
$verbose = $output->isVerbose();
$instanceId = $this->config->getSystemValueString('instanceid');
$output->writeln("This will migrate all previews from the old preview location to the new one.");
@@ -218,14 +221,21 @@ class Repair extends Command {
return 1;
}
$lockName = 'occ preview:repair lock ' . $oldPreviewFolder->getId();
try {
$section1->writeln(" Locking \"$lockName\" ", OutputInterface::VERBOSITY_VERBOSE);
$this->lockingProvider->acquireLock($lockName, ILockingProvider::LOCK_EXCLUSIVE);
} catch (LockedException $e) {
$section1->writeln(" Skipping because it is locked - another process seems to work on this ");
continue;
}
$previews = $oldPreviewFolder->getDirectoryListing();
if ($previews !== []) {
try {
$this->rootFolder->get("appdata_$instanceId/preview/$newFoldername");
} catch (NotFoundException $e) {
if ($verbose) {
$section1->writeln(" Create folder preview/$newFoldername");
}
$section1->writeln(" Create folder preview/$newFoldername", OutputInterface::VERBOSITY_VERBOSE);
if (!$dryMode) {
$this->rootFolder->newFolder("appdata_$instanceId/preview/$newFoldername");
}
@@ -240,9 +250,7 @@ class Repair extends Command {
$progressBar->advance();
continue;
}
if ($verbose) {
$section1->writeln(" Move preview/$name/$previewName to preview/$newFoldername");
}
$section1->writeln(" Move preview/$name/$previewName to preview/$newFoldername", OutputInterface::VERBOSITY_VERBOSE);
if (!$dryMode) {
try {
$preview->move("appdata_$instanceId/preview/$newFoldername/$previewName");
@@ -253,9 +261,7 @@ class Repair extends Command {
}
}
if ($oldPreviewFolder->getDirectoryListing() === []) {
if ($verbose) {
$section1->writeln(" Delete empty folder preview/$name");
}
$section1->writeln(" Delete empty folder preview/$name", OutputInterface::VERBOSITY_VERBOSE);
if (!$dryMode) {
try {
$oldPreviewFolder->delete();
@@ -264,6 +270,10 @@ class Repair extends Command {
}
}
}
$this->lockingProvider->releaseLock($lockName, ILockingProvider::LOCK_EXCLUSIVE);
$section1->writeln(" Unlocked", OutputInterface::VERBOSITY_VERBOSE);
$section1->writeln(" Finished migrating previews of file with fileId $name ");
$progressBar->advance();
}

View File

@@ -433,7 +433,7 @@ nav[role='navigation'] {
li {
position: relative;
cursor: pointer;
margin: 0 2px;
padding: 0 2px;
display: flex;
justify-content: center;
@@ -446,6 +446,9 @@ nav[role='navigation'] {
align-items: center;
justify-content: center;
opacity: .6;
// Make sure most app names dont ellipsize
letter-spacing: -0.5px;
font-size: 12px;
}
/* focused app visual feedback */
@@ -453,13 +456,21 @@ nav[role='navigation'] {
a:focus,
a.active {
opacity: 1;
font-weight: bold;
}
// Text size back to normal for hover/focus
&:hover a,
a:focus {
font-size: 14px;
}
&:hover a + span,
a:focus + span,
&:hover span,
&:focus span,
a:focus span {
a:focus span,
a.active span {
display: inline-block;
text-overflow: initial;
width: auto;
@@ -482,7 +493,7 @@ nav[role='navigation'] {
position: absolute;
color: var(--color-primary-text);
bottom: 2px;
width: calc(100% - 4px);
width: 100%;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;

Some files were not shown because too many files have changed in this diff Show More