Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Imageproxy to resolve relative Imagepaths #785

Merged
merged 32 commits into from
Jan 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c439693
add fileproxy to api
newhinton Nov 28, 2021
84ef831
fix image display
newhinton Nov 28, 2021
2284748
demo url update for api
newhinton Nov 28, 2021
09ece54
dont serve file outside of the notes app
newhinton Nov 28, 2021
e80e7de
properly generate nextcloud base url
newhinton Nov 28, 2021
0601004
pass noteid to markdown editor for url generation
newhinton Nov 28, 2021
b852367
fix calculation of relative images
newhinton Nov 28, 2021
5384a12
fix lint issues
newhinton Nov 29, 2021
52e2439
allow the client to upload files to a note
newhinton Nov 29, 2021
ad0da7e
add toolbar to ui
newhinton Nov 29, 2021
9cafa88
fix styleissues
newhinton Nov 29, 2021
818cb13
add local filepicker example
newhinton Dec 4, 2021
c2e4613
base64 encode imagepaths
newhinton Dec 4, 2021
0032991
finalize external api
newhinton Dec 5, 2021
044a62c
return statuscodes instead of json
newhinton Dec 5, 2021
e55d47c
fix api not processing uploads properly
newhinton Dec 5, 2021
0bce652
refactor NotesController and NotesService
newhinton Dec 5, 2021
84fab35
fully implement imagepicker from cloud
newhinton Dec 5, 2021
3200ead
remove toolbar
newhinton Dec 5, 2021
60047f9
fix linter issues
newhinton Dec 5, 2021
155ae24
also render files as download button
newhinton Dec 8, 2021
227b98c
fix minor style issues
newhinton Dec 11, 2021
aac2223
return correct http response
newhinton Dec 11, 2021
3fb67df
check if imagepath, but ignore case
newhinton Dec 11, 2021
176503f
check if attachment is in notepath of notesapp
newhinton Dec 16, 2021
1706cae
fix lowercasepath breaking non-image downloads
newhinton Dec 18, 2021
35d1599
fix js lint issues
newhinton Dec 18, 2021
366fafe
fix lint issues
newhinton Jan 20, 2022
e7014c8
code style and clean-up
korelstar Jan 20, 2022
dad79c8
clean up code
korelstar Jan 30, 2022
d05af13
simplify path check
korelstar Jan 30, 2022
d2fe4a3
improve path check
korelstar Jan 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@
'requirements' => ['id' => '\d+'],
],

////////// A T T A C H M E N T S //////////

[
'name' => 'notes#getAttachment',
'url' => '/notes/{noteid}/attachment',
'verb' => 'GET',
'requirements' => ['noteid' => '\d+'],
],
[
'name' => 'notes#uploadFile',
'url' => '/notes/{noteid}/attachment',
'verb' => 'POST',
'requirements' => ['noteid' => '\d+'],
],

////////// S E T T I N G S //////////
['name' => 'settings#set', 'url' => '/settings', 'verb' => 'PUT'],
Expand Down
36 changes: 36 additions & 0 deletions lib/Controller/NotesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use OCA\Notes\Service\SettingsService;

use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\IRequest;
use OCP\IConfig;
use OCP\IL10N;
Expand Down Expand Up @@ -296,4 +297,39 @@ public function destroy(int $id) : JSONResponse {
return [];
});
}

/**
* With help from: https://github.com/nextcloud/cookbook
* @NoAdminRequired
* @NoCSRFRequired
* @return JSONResponse|FileDisplayResponse
*/
public function getAttachment(int $noteid, string $path) {
try {
$targetimage = $this->notesService->getAttachment(
$this->helper->getUID(),
$noteid,
$path
);
$headers = ['Content-Type' => $targetimage->getMimetype(), 'Cache-Control' => 'public, max-age=604800'];
return new FileDisplayResponse($targetimage, Http::STATUS_OK, $headers);
} catch (\Exception $e) {
$this->helper->logException($e);
return $this->helper->createErrorResponse($e, Http::STATUS_NOT_FOUND);
}
}

/**
* @NoAdminRequired
*/
public function uploadFile(int $noteid): JSONResponse {
$file = $this->request->getUploadedFile('file');
return $this->helper->handleErrorResponse(function () use ($noteid, $file) {
return $this->notesService->createImage(
$this->helper->getUID(),
$noteid,
$file
);
});
}
}
57 changes: 57 additions & 0 deletions lib/Service/NotesService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use OCP\Files\File;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\NotPermittedException;

class NotesService {
private $metaService;
Expand Down Expand Up @@ -186,4 +187,60 @@ private static function getFileById(Folder $folder, int $id) : File {
}
return $file[0];
}

/**
* @NoAdminRequired
* @NoCSRFRequired
* @return \OCP\Files\File
*/
public function getAttachment(string $userId, int $noteid, string $path) : File {
$note = $this->get($userId, $noteid);
$notesFolder = $this->getNotesFolder($userId);
$path = str_replace('\\', '/', $path); // change windows style path
$p = explode('/', $note->getCategory());
// process relative target path
foreach (explode('/', $path) as $f) {
if ($f == '..') {
array_pop($p);
} elseif ($f !== '') {
array_push($p, $f);
}
}
$targetNode = $notesFolder->get(implode('/', $p));
assert($targetNode instanceof \OCP\Files\File);
return $targetNode;
}

/**
* @param $userId
* @param $noteid
* @param $fileDataArray
* @throws NotPermittedException
* https://github.com/nextcloud/deck/blob/master/lib/Service/AttachmentService.php
*/
public function createImage(string $userId, int $noteid, $fileDataArray) {
$note = $this->get($userId, $noteid);
$notesFolder = $this->getNotesFolder($userId);
$parent = $this->noteUtil->getCategoryFolder($notesFolder, $note->getCategory());

// try to generate long id, if not available on system fall back to a shorter one
try {
$filename = bin2hex(random_bytes(16));
} catch (\Exception $e) {
$filename = uniqid();
}
$filename = $filename . '.' . explode('.', $fileDataArray['name'])[1];

// read uploaded file from disk
$fp = fopen($fileDataArray['tmp_name'], 'r');
$content = fread($fp, $fileDataArray['size']);
fclose($fp);

$result = [];
$result['filename'] = $filename;
$result['filepath'] = $parent->getPath() . '/' . $filename;
$result['wasUploaded'] = true;

$this->noteUtil->getRoot()->newFile($parent->getPath() . '/' . $filename, $content);
}
}
65 changes: 65 additions & 0 deletions src/components/EditorEasyMDE.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<script>

import EasyMDE from 'easymde'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import store from '../store'

export default {
name: 'EditorEasyMDE',
Expand All @@ -19,6 +22,10 @@ export default {
type: Boolean,
required: true,
},
noteid: {
type: String,
required: true,
},
},

data() {
Expand Down Expand Up @@ -121,6 +128,64 @@ export default {
}
},

async onClickSelect() {
const apppath = '/' + store.state.app.settings.notesPath
const categories = store.getters.getCategories()
const currentNotePath = apppath + '/' + categories

const doc = this.mde.codemirror.getDoc()
const cursor = this.mde.codemirror.getCursor()
OC.dialogs.filepicker(
t('notes', 'Select an image'),
(path) => {

if (!path.startsWith(apppath)) {
OC.dialogs.alert(
t('notes', 'You cannot select images outside of your notes folder. Your notes folder is: {folder}', { folder: apppath }),
t('notes', 'Wrong Image'),
)
return
}
const noteLevel = ((currentNotePath + '/').split('/').length) - 1
const imageLevel = (path.split('/').length - 1)
const upwardsLevel = noteLevel - imageLevel
for (let i = 0; i < upwardsLevel; i++) {
path = '../' + path
}
path = path.replace(apppath + '/', '')
doc.replaceRange('![' + path + '](' + path + ')', { line: cursor.line })
},
false,
['image/jpeg', 'image/png'],
true,
OC.dialogs.FILEPICKER_TYPE_CHOOSE,
currentNotePath
)
},

async onClickUpload() {
const doc = this.mde.codemirror.getDoc()
const cursor = this.mde.codemirror.getCursor()
const id = this.noteid

const temporaryInput = document.createElement('input')
temporaryInput.setAttribute('type', 'file')
temporaryInput.onchange = async function() {
const data = new FormData()
data.append('file', temporaryInput.files[0])
const response = await axios({
method: 'POST',
url: generateUrl('apps/notes') + '/notes/' + id + '/attachment',
data,
})
const name = response.data[0].filename
const position = {
line: cursor.line,
}
doc.replaceRange('![' + name + '](' + name + ')', position)
}
temporaryInput.click()
},
},
}
</script>
Expand Down
75 changes: 75 additions & 0 deletions src/components/EditorMarkdownIt.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<script>

import MarkdownIt from 'markdown-it'
import { generateUrl } from '@nextcloud/router'

export default {
name: 'EditorMarkdownIt',
Expand All @@ -14,6 +15,10 @@ export default {
type: String,
required: true,
},
noteid: {
type: String,
required: true,
},
},

data() {
Expand All @@ -38,13 +43,47 @@ export default {
},

created() {
this.setImageRule(this.noteid)
this.onUpdate()
},

methods: {
onUpdate() {
this.html = this.md.render(this.value)
},
setImageRule(id) {
// https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
// Remember old renderer, if overridden, or proxy to default renderer
const defaultRender = this.md.renderer.rules.image || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options)
}

this.md.renderer.rules.image = function(tokens, idx, options, env, self) {
// If you are sure other plugins can't add `target` - drop check below
const token = tokens[idx]
const aIndex = token.attrIndex('src')
let path = token.attrs[aIndex][1]

if (!path.startsWith('http')) {
path = generateUrl('apps/notes/notes/{id}/attachment?path={path}', { id, path })
}

token.attrs[aIndex][1] = path
const lowecasePath = path.toLowerCase()
// pass token to default renderer.
if (lowecasePath.endsWith('jpg')
|| lowecasePath.endsWith('jpeg')
|| lowecasePath.endsWith('bmp')
|| lowecasePath.endsWith('webp')
|| lowecasePath.endsWith('gif')
|| lowecasePath.endsWith('png')) {
return defaultRender(tokens, idx, options, env, self)
} else {
const dlimgpath = generateUrl('svg/core/actions/download?color=ffffff')
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>'
}
}
},
},

}
Expand Down Expand Up @@ -145,5 +184,41 @@ export default {
cursor: default;
}
}

& img {
width: 75%;
margin-left: auto;
margin-right: auto;
display: block;
}

.download-file {
width: 75%;
margin-left: auto;
margin-right: auto;
display: block;
text-align: center;
}

.download-icon {
padding: 15px;
margin-left: auto;
margin-right: auto;
width: 75%;
border-radius: 10px;
background-color: var(--color-background-dark);
border: 1px solid transparent; // so that it does not move on hover
}

.download-icon:hover {
border: 1px var(--color-primary-element) solid;
}

.download-icon-inner {
height: 3em;
width: auto;
margin-bottom: 5px;
}

}
</style>
3 changes: 2 additions & 1 deletion src/components/Note.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@
<div v-show="!note.content" class="placeholder">
{{ preview ? t('notes', 'Empty note') : t('notes', 'Write …') }}
</div>
<ThePreview v-if="preview" :value="note.content" />
<ThePreview v-if="preview" :value="note.content" :noteid="noteId" />
<TheEditor v-else
:value="note.content"
:noteid="noteId"
:readonly="note.readonly"
@input="onEdit"
/>
Expand Down