Skip to content

Commit

Permalink
Add Imageproxy to resolve relative Imagepaths
Browse files Browse the repository at this point in the history
  • Loading branch information
newhinton authored Jan 30, 2022
1 parent 592911b commit 34d3cc1
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 1 deletion.
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

0 comments on commit 34d3cc1

Please sign in to comment.