Skip to content

Commit 34d3cc1

Browse files
authored
Add Imageproxy to resolve relative Imagepaths
1 parent 592911b commit 34d3cc1

File tree

6 files changed

+249
-1
lines changed

6 files changed

+249
-1
lines changed

appinfo/routes.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@
8383
'requirements' => ['id' => '\d+'],
8484
],
8585

86+
////////// A T T A C H M E N T S //////////
87+
88+
[
89+
'name' => 'notes#getAttachment',
90+
'url' => '/notes/{noteid}/attachment',
91+
'verb' => 'GET',
92+
'requirements' => ['noteid' => '\d+'],
93+
],
94+
[
95+
'name' => 'notes#uploadFile',
96+
'url' => '/notes/{noteid}/attachment',
97+
'verb' => 'POST',
98+
'requirements' => ['noteid' => '\d+'],
99+
],
86100

87101
////////// S E T T I N G S //////////
88102
['name' => 'settings#set', 'url' => '/settings', 'verb' => 'PUT'],

lib/Controller/NotesController.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use OCA\Notes\Service\SettingsService;
1010

1111
use OCP\AppFramework\Controller;
12+
use OCP\AppFramework\Http\FileDisplayResponse;
1213
use OCP\IRequest;
1314
use OCP\IConfig;
1415
use OCP\IL10N;
@@ -296,4 +297,39 @@ public function destroy(int $id) : JSONResponse {
296297
return [];
297298
});
298299
}
300+
301+
/**
302+
* With help from: https://github.com/nextcloud/cookbook
303+
* @NoAdminRequired
304+
* @NoCSRFRequired
305+
* @return JSONResponse|FileDisplayResponse
306+
*/
307+
public function getAttachment(int $noteid, string $path) {
308+
try {
309+
$targetimage = $this->notesService->getAttachment(
310+
$this->helper->getUID(),
311+
$noteid,
312+
$path
313+
);
314+
$headers = ['Content-Type' => $targetimage->getMimetype(), 'Cache-Control' => 'public, max-age=604800'];
315+
return new FileDisplayResponse($targetimage, Http::STATUS_OK, $headers);
316+
} catch (\Exception $e) {
317+
$this->helper->logException($e);
318+
return $this->helper->createErrorResponse($e, Http::STATUS_NOT_FOUND);
319+
}
320+
}
321+
322+
/**
323+
* @NoAdminRequired
324+
*/
325+
public function uploadFile(int $noteid): JSONResponse {
326+
$file = $this->request->getUploadedFile('file');
327+
return $this->helper->handleErrorResponse(function () use ($noteid, $file) {
328+
return $this->notesService->createImage(
329+
$this->helper->getUID(),
330+
$noteid,
331+
$file
332+
);
333+
});
334+
}
299335
}

lib/Service/NotesService.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use OCP\Files\File;
88
use OCP\Files\FileInfo;
99
use OCP\Files\Folder;
10+
use OCP\Files\NotPermittedException;
1011

1112
class NotesService {
1213
private $metaService;
@@ -186,4 +187,60 @@ private static function getFileById(Folder $folder, int $id) : File {
186187
}
187188
return $file[0];
188189
}
190+
191+
/**
192+
* @NoAdminRequired
193+
* @NoCSRFRequired
194+
* @return \OCP\Files\File
195+
*/
196+
public function getAttachment(string $userId, int $noteid, string $path) : File {
197+
$note = $this->get($userId, $noteid);
198+
$notesFolder = $this->getNotesFolder($userId);
199+
$path = str_replace('\\', '/', $path); // change windows style path
200+
$p = explode('/', $note->getCategory());
201+
// process relative target path
202+
foreach (explode('/', $path) as $f) {
203+
if ($f == '..') {
204+
array_pop($p);
205+
} elseif ($f !== '') {
206+
array_push($p, $f);
207+
}
208+
}
209+
$targetNode = $notesFolder->get(implode('/', $p));
210+
assert($targetNode instanceof \OCP\Files\File);
211+
return $targetNode;
212+
}
213+
214+
/**
215+
* @param $userId
216+
* @param $noteid
217+
* @param $fileDataArray
218+
* @throws NotPermittedException
219+
* https://github.com/nextcloud/deck/blob/master/lib/Service/AttachmentService.php
220+
*/
221+
public function createImage(string $userId, int $noteid, $fileDataArray) {
222+
$note = $this->get($userId, $noteid);
223+
$notesFolder = $this->getNotesFolder($userId);
224+
$parent = $this->noteUtil->getCategoryFolder($notesFolder, $note->getCategory());
225+
226+
// try to generate long id, if not available on system fall back to a shorter one
227+
try {
228+
$filename = bin2hex(random_bytes(16));
229+
} catch (\Exception $e) {
230+
$filename = uniqid();
231+
}
232+
$filename = $filename . '.' . explode('.', $fileDataArray['name'])[1];
233+
234+
// read uploaded file from disk
235+
$fp = fopen($fileDataArray['tmp_name'], 'r');
236+
$content = fread($fp, $fileDataArray['size']);
237+
fclose($fp);
238+
239+
$result = [];
240+
$result['filename'] = $filename;
241+
$result['filepath'] = $parent->getPath() . '/' . $filename;
242+
$result['wasUploaded'] = true;
243+
244+
$this->noteUtil->getRoot()->newFile($parent->getPath() . '/' . $filename, $content);
245+
}
189246
}

src/components/EditorEasyMDE.vue

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
<script>
77
88
import EasyMDE from 'easymde'
9+
import axios from '@nextcloud/axios'
10+
import { generateUrl } from '@nextcloud/router'
11+
import store from '../store'
912
1013
export default {
1114
name: 'EditorEasyMDE',
@@ -19,6 +22,10 @@ export default {
1922
type: Boolean,
2023
required: true,
2124
},
25+
noteid: {
26+
type: String,
27+
required: true,
28+
},
2229
},
2330
2431
data() {
@@ -121,6 +128,64 @@ export default {
121128
}
122129
},
123130
131+
async onClickSelect() {
132+
const apppath = '/' + store.state.app.settings.notesPath
133+
const categories = store.getters.getCategories()
134+
const currentNotePath = apppath + '/' + categories
135+
136+
const doc = this.mde.codemirror.getDoc()
137+
const cursor = this.mde.codemirror.getCursor()
138+
OC.dialogs.filepicker(
139+
t('notes', 'Select an image'),
140+
(path) => {
141+
142+
if (!path.startsWith(apppath)) {
143+
OC.dialogs.alert(
144+
t('notes', 'You cannot select images outside of your notes folder. Your notes folder is: {folder}', { folder: apppath }),
145+
t('notes', 'Wrong Image'),
146+
)
147+
return
148+
}
149+
const noteLevel = ((currentNotePath + '/').split('/').length) - 1
150+
const imageLevel = (path.split('/').length - 1)
151+
const upwardsLevel = noteLevel - imageLevel
152+
for (let i = 0; i < upwardsLevel; i++) {
153+
path = '../' + path
154+
}
155+
path = path.replace(apppath + '/', '')
156+
doc.replaceRange('![' + path + '](' + path + ')', { line: cursor.line })
157+
},
158+
false,
159+
['image/jpeg', 'image/png'],
160+
true,
161+
OC.dialogs.FILEPICKER_TYPE_CHOOSE,
162+
currentNotePath
163+
)
164+
},
165+
166+
async onClickUpload() {
167+
const doc = this.mde.codemirror.getDoc()
168+
const cursor = this.mde.codemirror.getCursor()
169+
const id = this.noteid
170+
171+
const temporaryInput = document.createElement('input')
172+
temporaryInput.setAttribute('type', 'file')
173+
temporaryInput.onchange = async function() {
174+
const data = new FormData()
175+
data.append('file', temporaryInput.files[0])
176+
const response = await axios({
177+
method: 'POST',
178+
url: generateUrl('apps/notes') + '/notes/' + id + '/attachment',
179+
data,
180+
})
181+
const name = response.data[0].filename
182+
const position = {
183+
line: cursor.line,
184+
}
185+
doc.replaceRange('![' + name + '](' + name + ')', position)
186+
}
187+
temporaryInput.click()
188+
},
124189
},
125190
}
126191
</script>

src/components/EditorMarkdownIt.vue

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<script>
66
77
import MarkdownIt from 'markdown-it'
8+
import { generateUrl } from '@nextcloud/router'
89
910
export default {
1011
name: 'EditorMarkdownIt',
@@ -14,6 +15,10 @@ export default {
1415
type: String,
1516
required: true,
1617
},
18+
noteid: {
19+
type: String,
20+
required: true,
21+
},
1722
},
1823
1924
data() {
@@ -38,13 +43,47 @@ export default {
3843
},
3944
4045
created() {
46+
this.setImageRule(this.noteid)
4147
this.onUpdate()
4248
},
4349
4450
methods: {
4551
onUpdate() {
4652
this.html = this.md.render(this.value)
4753
},
54+
setImageRule(id) {
55+
// https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
56+
// Remember old renderer, if overridden, or proxy to default renderer
57+
const defaultRender = this.md.renderer.rules.image || function(tokens, idx, options, env, self) {
58+
return self.renderToken(tokens, idx, options)
59+
}
60+
61+
this.md.renderer.rules.image = function(tokens, idx, options, env, self) {
62+
// If you are sure other plugins can't add `target` - drop check below
63+
const token = tokens[idx]
64+
const aIndex = token.attrIndex('src')
65+
let path = token.attrs[aIndex][1]
66+
67+
if (!path.startsWith('http')) {
68+
path = generateUrl('apps/notes/notes/{id}/attachment?path={path}', { id, path })
69+
}
70+
71+
token.attrs[aIndex][1] = path
72+
const lowecasePath = path.toLowerCase()
73+
// pass token to default renderer.
74+
if (lowecasePath.endsWith('jpg')
75+
|| lowecasePath.endsWith('jpeg')
76+
|| lowecasePath.endsWith('bmp')
77+
|| lowecasePath.endsWith('webp')
78+
|| lowecasePath.endsWith('gif')
79+
|| lowecasePath.endsWith('png')) {
80+
return defaultRender(tokens, idx, options, env, self)
81+
} else {
82+
const dlimgpath = generateUrl('svg/core/actions/download?color=ffffff')
83+
return '<div class="download-file"><a href="' + path.replace(/"/g, '&quot;') + '"><div class="download-icon"><img class="download-icon-inner" src="' + dlimgpath + '">' + token.content + '</div></a></div>'
84+
}
85+
}
86+
},
4887
},
4988
5089
}
@@ -145,5 +184,41 @@ export default {
145184
cursor: default;
146185
}
147186
}
187+
188+
& img {
189+
width: 75%;
190+
margin-left: auto;
191+
margin-right: auto;
192+
display: block;
193+
}
194+
195+
.download-file {
196+
width: 75%;
197+
margin-left: auto;
198+
margin-right: auto;
199+
display: block;
200+
text-align: center;
201+
}
202+
203+
.download-icon {
204+
padding: 15px;
205+
margin-left: auto;
206+
margin-right: auto;
207+
width: 75%;
208+
border-radius: 10px;
209+
background-color: var(--color-background-dark);
210+
border: 1px solid transparent; // so that it does not move on hover
211+
}
212+
213+
.download-icon:hover {
214+
border: 1px var(--color-primary-element) solid;
215+
}
216+
217+
.download-icon-inner {
218+
height: 3em;
219+
width: auto;
220+
margin-bottom: 5px;
221+
}
222+
148223
}
149224
</style>

src/components/Note.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@
3030
<div v-show="!note.content" class="placeholder">
3131
{{ preview ? t('notes', 'Empty note') : t('notes', 'Write …') }}
3232
</div>
33-
<ThePreview v-if="preview" :value="note.content" />
33+
<ThePreview v-if="preview" :value="note.content" :noteid="noteId" />
3434
<TheEditor v-else
3535
:value="note.content"
36+
:noteid="noteId"
3637
:readonly="note.readonly"
3738
@input="onEdit"
3839
/>

0 commit comments

Comments
 (0)