Skip to content

Commit ad15a91

Browse files
committed
fix: Load preview URL for download-disabled shares
This was possible on Nextcloud 30 and previous due to a "bug": The `download` permission was simply not rejected for public shares, just a `hide` flag was set on the public share. Now the permissions are correctly set, so loading a preview is not possible. The work-around is to allow previews when the correct header is set. Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent b6722ac commit ad15a91

11 files changed

+162
-275
lines changed

cypress/e2e/download-forbidden.cy.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ describe('Disable download button if forbidden', { testIsolation: true }, () =>
3535
.should('contain', 'image1 .jpg')
3636
})
3737

38-
// TODO: Fix no-download files on server
39-
it.skip('See the image can be shown', () => {
38+
it('See the image can be shown', () => {
4039
cy.getFile('image1.jpg').should('be.visible')
4140
cy.openFile('image1.jpg')
4241
cy.get('body > .viewer').should('be.visible')

cypress/e2e/sharing/download-share-disabled.cy.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,14 @@ describe(`Download ${fileName} in viewer`, function() {
110110
cy.get('body > .viewer').should('be.visible')
111111
})
112112

113-
// TODO: FIX DOWNLOAD DISABLED SHARES
114-
it.skip('Does not see a loading animation', function() {
113+
it('Does not see a loading animation', function() {
115114
cy.get('body > .viewer', { timeout: 10000 })
116115
.should('be.visible')
117116
.and('have.class', 'modal-mask')
118117
.and('not.have.class', 'icon-loading')
119118
})
120119

121-
// TODO: FIX DOWNLOAD DISABLED SHARES
122-
it.skip('See the title on the viewer header but not the Download nor the menu button', function() {
120+
it('See the title on the viewer header but not the Download nor the menu button', function() {
123121
cy.get('body > .viewer .modal-header__name').should('contain', 'image1.jpg')
124122
cy.get('body a[download="image1.jpg"]').should('not.exist')
125123
cy.get('body > .viewer .modal-header button.action-item__menutoggle').should('not.exist')

src/components/Images.vue

+70-55
Original file line numberDiff line numberDiff line change
@@ -28,63 +28,63 @@
2828
:fileid="fileid"
2929
@close="onClose" />
3030

31-
<template v-else-if="data !== null">
32-
<img v-if="!livePhotoCanBePlayed"
33-
ref="image"
34-
:alt="alt"
31+
<IconImageBroken v-if="!data" :size="64" />
32+
33+
<img v-else-if="!livePhotoCanBePlayed"
34+
ref="image"
35+
:alt="alt"
36+
:class="{
37+
dragging,
38+
loaded,
39+
zoomed: zoomRatio > 1
40+
}"
41+
:src="data"
42+
:style="imgStyle"
43+
@error.capture.prevent.stop.once="onFail"
44+
@load="updateImgSize"
45+
@wheel.stop.prevent="updateZoom"
46+
@dblclick.prevent="onDblclick"
47+
@pointerdown.prevent="pointerDown"
48+
@pointerup.prevent="pointerUp"
49+
@pointermove.prevent="pointerMove">
50+
51+
<template v-else-if="livePhoto">
52+
<video v-show="livePhotoCanBePlayed"
53+
ref="video"
3554
:class="{
3655
dragging,
3756
loaded,
3857
zoomed: zoomRatio > 1
3958
}"
40-
:src="data"
4159
:style="imgStyle"
42-
@error.capture.prevent.stop.once="onFail"
43-
@load="updateImgSize"
60+
:playsinline="true"
61+
:poster="data"
62+
:src="livePhotoSrc"
63+
preload="metadata"
64+
@canplaythrough="doneLoadingLivePhoto"
65+
@loadedmetadata="updateImgSize"
4466
@wheel.stop.prevent="updateZoom"
67+
@error.capture.prevent.stop.once="onFail"
4568
@dblclick.prevent="onDblclick"
4669
@pointerdown.prevent="pointerDown"
4770
@pointerup.prevent="pointerUp"
48-
@pointermove.prevent="pointerMove">
49-
50-
<template v-if="livePhoto">
51-
<video v-show="livePhotoCanBePlayed"
52-
ref="video"
53-
:class="{
54-
dragging,
55-
loaded,
56-
zoomed: zoomRatio > 1
57-
}"
58-
:style="imgStyle"
59-
:playsinline="true"
60-
:poster="data"
61-
:src="livePhotoSrc"
62-
preload="metadata"
63-
@canplaythrough="doneLoadingLivePhoto"
64-
@loadedmetadata="updateImgSize"
65-
@wheel.stop.prevent="updateZoom"
66-
@error.capture.prevent.stop.once="onFail"
67-
@dblclick.prevent="onDblclick"
68-
@pointerdown.prevent="pointerDown"
69-
@pointerup.prevent="pointerUp"
70-
@pointermove.prevent="pointerMove"
71-
@ended="stopLivePhoto" />
72-
<button v-if="width !== 0"
73-
class="live-photo_play_button"
74-
:style="{left: `calc(50% - ${width/2}px)`}"
75-
:disabled="!livePhotoCanBePlayed"
76-
:aria-description="t('viewer', 'Play the live photo')"
77-
@click="playLivePhoto"
78-
@pointerenter="playLivePhoto"
79-
@focus="playLivePhoto"
80-
@pointerleave="stopLivePhoto"
81-
@blur="stopLivePhoto">
82-
<PlayCircleOutline v-if="livePhotoCanBePlayed" />
83-
<NcLoadingIcon v-else />
84-
<!-- TRANSLATORS Label of the button used at the top left corner of live photos to play them -->
85-
{{ t('viewer', 'LIVE') }}
86-
</button>
87-
</template>
71+
@pointermove.prevent="pointerMove"
72+
@ended="stopLivePhoto" />
73+
<button v-if="width !== 0"
74+
class="live-photo_play_button"
75+
:style="{left: `calc(50% - ${width/2}px)`}"
76+
:disabled="!livePhotoCanBePlayed"
77+
:aria-description="t('viewer', 'Play the live photo')"
78+
@click="playLivePhoto"
79+
@pointerenter="playLivePhoto"
80+
@focus="playLivePhoto"
81+
@pointerleave="stopLivePhoto"
82+
@blur="stopLivePhoto">
83+
<PlayCircleOutline v-if="livePhotoCanBePlayed" />
84+
<NcLoadingIcon v-else />
85+
<!-- TRANSLATORS Label of the button used at the top left corner of live photos to play them -->
86+
{{ t('viewer', 'LIVE') }}
87+
</button>
8888
</template>
8989
</div>
9090
</template>
@@ -93,6 +93,7 @@
9393
import Vue from 'vue'
9494
import AsyncComputed from 'vue-async-computed'
9595
import PlayCircleOutline from 'vue-material-design-icons/PlayCircleOutline.vue'
96+
import IconImageBroken from 'vue-material-design-icons/ImageBroken.vue'
9697

9798
import axios from '@nextcloud/axios'
9899
import { basename } from '@nextcloud/paths'
@@ -102,13 +103,15 @@ import { NcLoadingIcon } from '@nextcloud/vue'
102103
import ImageEditor from './ImageEditor.vue'
103104
import { findLivePhotoPeerFromFileId } from '../utils/livePhotoUtils'
104105
import { getDavPath } from '../utils/fileUtils'
106+
import { getPreviewIfAny } from '../utils/previewUtils'
105107

106108
Vue.use(AsyncComputed)
107109

108110
export default {
109111
name: 'Images',
110112

111113
components: {
114+
IconImageBroken,
112115
ImageEditor,
113116
PlayCircleOutline,
114117
NcLoadingIcon,
@@ -192,23 +195,35 @@ export default {
192195

193196
// Load the raw gif instead of the static preview
194197
if (this.mime === 'image/gif') {
198+
// if the source failed fallback to the preview
199+
if (this.fallback) {
200+
return this.previewPath
201+
}
195202
return this.src
196203
}
197204

198-
// If there is no preview and we have a direct source
199-
// load it instead
200-
if (this.source && !this.hasPreview && !this.previewUrl) {
201-
return this.source
205+
// First try the preview if any
206+
if (!this.fallback && this.previewPath) {
207+
return this.previewPath
202208
}
203209

204210
// If loading the preview failed once, let's load the original file
205-
if (this.fallback) {
206-
return this.src
207-
}
211+
return this.src
212+
},
208213

209-
return this.previewPath
214+
async previewPath() {
215+
return await getPreviewIfAny({
216+
...this.$attrs,
217+
fileid: this.fileid,
218+
filename: this.filename,
219+
previewUrl: this.previewUrl,
220+
hasPreview: this.hasPreview,
221+
davPath: this.davPath,
222+
etag: this.$attrs.etag,
223+
})
210224
},
211225
},
226+
212227
watch: {
213228
active(val, old) {
214229
// the item was hidden before and is now the current view

src/mixins/Mime.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@
2020
*
2121
*/
2222
import debounce from 'debounce'
23-
import PreviewUrl from '../mixins/PreviewUrl.js'
2423
import parsePath from 'path-parse'
24+
import { getDavPath } from '../utils/fileUtils.ts'
2525

2626
export default {
2727
inheritAttrs: false,
28-
mixins: [PreviewUrl],
2928
props: {
3029
// Is the current component shown
3130
active: {
@@ -115,6 +114,18 @@ export default {
115114
},
116115

117116
computed: {
117+
/**
118+
* Absolute dav remote path of the file
119+
*
120+
* @return {string}
121+
*/
122+
davPath() {
123+
return getDavPath({
124+
filename: this.filename,
125+
basename: this.basename,
126+
})
127+
},
128+
118129
name() {
119130
return parsePath(this.basename).name
120131
},

src/mixins/PreviewUrl.js

-73
This file was deleted.

src/utils/fileUtils.ts

+16-33
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import { getLanguage } from '@nextcloud/l10n'
2626
import { encodePath } from '@nextcloud/paths'
2727
import camelcase from 'camelcase'
2828

29-
import { isNumber } from './numberUtil'
30-
3129
export interface FileInfo {
3230
/** ID of the file (not unique if shared, use source instead) */
3331
fileid?: number
@@ -95,13 +93,7 @@ export function sortCompare(fileInfo1: FileInfo, fileInfo2: FileInfo, key: strin
9593
return 1
9694
}
9795

98-
// if this is a number, let's sort by integer
99-
if (isNumber(fileInfo1[key]) && isNumber(fileInfo2[key])) {
100-
const result = Number(fileInfo1[key]) - Number(fileInfo2[key])
101-
return asc ? result : -result
102-
}
103-
104-
// else we sort by string, so let's sort directories first
96+
// let's sort directories first
10597
if (fileInfo1.type === 'directory' && fileInfo2.type !== 'directory') {
10698
return -1
10799
} else if (fileInfo1.type !== 'directory' && fileInfo2.type === 'directory') {
@@ -114,8 +106,8 @@ export function sortCompare(fileInfo1: FileInfo, fileInfo2: FileInfo, key: strin
114106
}
115107
// finally sort by name
116108
return asc
117-
? fileInfo1[key].localeCompare(fileInfo2[key], getLanguage(), { numeric: true })
118-
: -fileInfo1[key].localeCompare(fileInfo2[key], getLanguage(), { numeric: true })
109+
? String(fileInfo1[key]).localeCompare(fileInfo2[key], getLanguage(), { numeric: true })
110+
: -String(fileInfo1[key]).localeCompare(fileInfo2[key], getLanguage(), { numeric: true })
119111
}
120112

121113
/**
@@ -124,29 +116,20 @@ export function sortCompare(fileInfo1: FileInfo, fileInfo2: FileInfo, key: strin
124116
* @param obj The stat response to convert
125117
*/
126118
export function genFileInfo(obj: FileStat): FileInfo {
127-
const fileInfo = {}
128-
129-
Object.keys(obj).forEach(key => {
130-
const data = obj[key]
131-
132-
// flatten object if any
133-
if (!!data && typeof data === 'object' && !Array.isArray(data)) {
134-
Object.assign(fileInfo, genFileInfo(data))
135-
} else {
136-
// format key and add it to the fileInfo
137-
if (data === 'false') {
138-
fileInfo[camelcase(key)] = false
139-
} else if (data === 'true') {
140-
fileInfo[camelcase(key)] = true
141-
} else {
142-
fileInfo[camelcase(key)] = isNumber(data)
143-
? Number(data)
144-
: data
145-
}
146-
}
147-
})
119+
const fileStat = {
120+
...(obj.props ?? {}),
121+
...obj,
122+
props: undefined,
123+
}
148124

149-
return fileInfo as FileInfo
125+
const fileInfo = Object.entries(fileStat)
126+
// Make property names camel case
127+
.map(([key, value]) => [camelcase(key), value])
128+
// Convert boolean - Numbers are already parsed by the WebDAV client
129+
.map(([key, value]) => [key, ['true', 'false'].includes(value as never) ? value === 'true' : value])
130+
// remove undefined properties
131+
.filter(([, value]) => value !== undefined)
132+
return Object.fromEntries(fileInfo)
150133
}
151134

152135
/**

0 commit comments

Comments
 (0)