Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccffe1094e | ||
|
|
b698c42a1d | ||
|
|
454a9115d8 | ||
|
|
e359c0582a | ||
|
|
3d912f4af1 | ||
|
|
6e60c5ab4b | ||
|
|
770d34814d | ||
|
|
49569150cb | ||
|
|
1beed6be61 | ||
|
|
fc85a09f69 | ||
|
|
8719f7a592 | ||
|
|
bf002b773f | ||
|
|
4e2577ce53 | ||
|
|
11e642af56 | ||
|
|
0956e6b2f6 | ||
|
|
217c1b3104 | ||
|
|
f5452a63be | ||
|
|
7162f1185d | ||
|
|
9d5ebf8877 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,6 +1,25 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 1.3.2 - 2021-04-07
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#2869](https://github.com/nextcloud/deck/pull/2869) Don't close tempfile as it is already done
|
||||
* [#2880](https://github.com/nextcloud/deck/pull/2880) Only extract additional attributes from query when doing a raw search
|
||||
* [#2924](https://github.com/nextcloud/deck/pull/2924) Avoid reusing the existing route object to make navigation work properly
|
||||
* [#2946](https://github.com/nextcloud/deck/pull/2946) Fix bug when saving the description
|
||||
|
||||
## 1.3.1 - 2021-03-05
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#2827](https://github.com/nextcloud/deck/pull/2827) Also include /apps/spreed urls in the listener for loading the talk integration
|
||||
* [#2844](https://github.com/nextcloud/deck/pull/2844) Search by mail on the board sharing input
|
||||
* [#2849](https://github.com/nextcloud/deck/pull/2849) Switch to Content-Disposition attachment and check for sane mimetypes
|
||||
* [#2859](https://github.com/nextcloud/deck/pull/2859) Properly pass the user to fetch circles when calling through occ
|
||||
|
||||
|
||||
## 1.3.0 - 2021-02-20
|
||||
|
||||
### Added
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- 🚀 Get your project organized
|
||||
|
||||
</description>
|
||||
<version>1.3.0</version>
|
||||
<version>1.3.2</version>
|
||||
<licence>agpl</licence>
|
||||
<author>Julius Härtl</author>
|
||||
<namespace>Deck</namespace>
|
||||
|
||||
@@ -169,7 +169,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
|
||||
}
|
||||
$circles = array_map(function ($circle) {
|
||||
return $circle->getUniqueId();
|
||||
}, \OCA\Circles\Api\v1\Circles::joinedCircles('', true));
|
||||
}, \OCA\Circles\Api\v1\Circles::joinedCircles($userId, true));
|
||||
if (count($circles) === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -149,8 +149,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
|
||||
public function queryCardsByBoards(array $boardIds): IQueryBuilder {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('c.*', 's.board_id')
|
||||
->selectAlias('s.title', 'stack_title')
|
||||
$qb->select('c.*')
|
||||
->from('deck_cards', 'c')
|
||||
->innerJoin('c', 'deck_stacks', 's', $qb->expr()->eq('s.id', 'c.stack_id'))
|
||||
->andWhere($qb->expr()->in('s.board_id', $qb->createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
@@ -281,7 +280,9 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
}
|
||||
|
||||
public function searchRaw($boardIds, $term, $limit = null, $offset = null) {
|
||||
$qb = $this->queryCardsByBoards($boardIds);
|
||||
$qb = $this->queryCardsByBoards($boardIds)
|
||||
->select('s.board_id', 'board_id')
|
||||
->selectAlias('s.title', 'stack_title');
|
||||
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
|
||||
@@ -49,11 +49,12 @@ class BeforeTemplateRenderedListener implements IEventListener {
|
||||
}
|
||||
Util::addStyle('deck', 'deck');
|
||||
|
||||
if (strpos($this->request->getPathInfo(), '/apps/calendar') === 0) {
|
||||
$pathInfo = $this->request->getPathInfo();
|
||||
if (strpos($pathInfo, '/apps/calendar') === 0) {
|
||||
Util::addScript('deck', 'calendar');
|
||||
}
|
||||
|
||||
if (strpos($this->request->getPathInfo(), '/call/') === 0) {
|
||||
if (strpos($pathInfo, '/call/') === 0 || strpos($pathInfo, '/apps/spreed') === 0) {
|
||||
Util::addScript('deck', 'talk');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,9 @@ use OCA\Deck\Db\Attachment;
|
||||
use OCA\Deck\Db\AttachmentMapper;
|
||||
use OCA\Deck\StatusException;
|
||||
use OCA\Deck\Exceptions\ConflictException;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\FileDisplayResponse;
|
||||
use OCP\AppFramework\Http\StreamResponse;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\IMimeTypeDetector;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
@@ -49,6 +48,7 @@ class FileService implements IAttachmentService {
|
||||
private $rootFolder;
|
||||
private $config;
|
||||
private $attachmentMapper;
|
||||
private $mimeTypeDetector;
|
||||
|
||||
public function __construct(
|
||||
IL10N $l10n,
|
||||
@@ -57,7 +57,8 @@ class FileService implements IAttachmentService {
|
||||
ILogger $logger,
|
||||
IRootFolder $rootFolder,
|
||||
IConfig $config,
|
||||
AttachmentMapper $attachmentMapper
|
||||
AttachmentMapper $attachmentMapper,
|
||||
IMimeTypeDetector $mimeTypeDetector
|
||||
) {
|
||||
$this->l10n = $l10n;
|
||||
$this->appData = $appData;
|
||||
@@ -66,6 +67,7 @@ class FileService implements IAttachmentService {
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->config = $config;
|
||||
$this->attachmentMapper = $attachmentMapper;
|
||||
$this->mimeTypeDetector = $mimeTypeDetector;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,27 +227,14 @@ class FileService implements IAttachmentService {
|
||||
|
||||
/**
|
||||
* @param Attachment $attachment
|
||||
* @return FileDisplayResponse|\OCP\AppFramework\Http\Response|StreamResponse
|
||||
* @return StreamResponse
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function display(Attachment $attachment) {
|
||||
$file = $this->getFileFromRootFolder($attachment);
|
||||
if (method_exists($file, 'fopen')) {
|
||||
$response = new StreamResponse($file->fopen('r'));
|
||||
$response->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
|
||||
} else {
|
||||
$response = new FileDisplayResponse($file);
|
||||
}
|
||||
// We need those since otherwise chrome won't show the PDF file with CSP rule object-src 'none'
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=271452
|
||||
$policy = new ContentSecurityPolicy();
|
||||
$policy->addAllowedObjectDomain('\'self\'');
|
||||
$policy->addAllowedObjectDomain('blob:');
|
||||
$policy->addAllowedMediaDomain('\'self\'');
|
||||
$policy->addAllowedMediaDomain('blob:');
|
||||
$response->setContentSecurityPolicy($policy);
|
||||
|
||||
$response->addHeader('Content-Type', $file->getMimeType());
|
||||
$response = new StreamResponse($file->fopen('rb'));
|
||||
$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
|
||||
$response->addHeader('Content-Type', $this->mimeTypeDetector->getSecureMimeType($file->getMimeType()));
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,9 @@ namespace OCA\Deck\Service;
|
||||
use OCA\Deck\Db\Attachment;
|
||||
use OCA\Deck\Sharing\DeckShareProvider;
|
||||
use OCA\Deck\StatusException;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\FileDisplayResponse;
|
||||
use OCP\AppFramework\Http\StreamResponse;
|
||||
use OCP\Constants;
|
||||
use OCP\Files\IMimeTypeDetector;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\IDBConnection;
|
||||
@@ -50,6 +49,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||
private $l10n;
|
||||
private $preview;
|
||||
private $permissionService;
|
||||
private $mimeTypeDetector;
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
@@ -60,6 +60,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||
DeckShareProvider $shareProvider,
|
||||
IPreview $preview,
|
||||
PermissionService $permissionService,
|
||||
IMimeTypeDetector $mimeTypeDetector,
|
||||
string $userId = null
|
||||
) {
|
||||
$this->request = $request;
|
||||
@@ -70,6 +71,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||
$this->shareManager = $shareManager;
|
||||
$this->userId = $userId;
|
||||
$this->preview = $preview;
|
||||
$this->mimeTypeDetector = $mimeTypeDetector;
|
||||
}
|
||||
|
||||
public function listAttachments(int $cardId): array {
|
||||
@@ -147,22 +149,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||
if ($file === null || $share->getSharedWith() !== (string)$attachment->getCardId()) {
|
||||
throw new NotFoundException('File not found');
|
||||
}
|
||||
if (method_exists($file, 'fopen')) {
|
||||
$response = new StreamResponse($file->fopen('r'));
|
||||
$response->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
|
||||
} else {
|
||||
$response = new FileDisplayResponse($file);
|
||||
}
|
||||
// We need those since otherwise chrome won't show the PDF file with CSP rule object-src 'none'
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=271452
|
||||
$policy = new ContentSecurityPolicy();
|
||||
$policy->addAllowedObjectDomain('\'self\'');
|
||||
$policy->addAllowedObjectDomain('blob:');
|
||||
$policy->addAllowedMediaDomain('\'self\'');
|
||||
$policy->addAllowedMediaDomain('blob:');
|
||||
$response->setContentSecurityPolicy($policy);
|
||||
|
||||
$response->addHeader('Content-Type', $file->getMimeType());
|
||||
$response = new StreamResponse($file->fopen('rb'));
|
||||
$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
|
||||
$response->addHeader('Content-Type', $this->mimeTypeDetector->getSecureMimeType($file->getMimeType()));
|
||||
return $response;
|
||||
}
|
||||
|
||||
@@ -184,7 +174,6 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||
throw new StatusException('Could not read file');
|
||||
}
|
||||
$target->putContent($content);
|
||||
fclose($content);
|
||||
|
||||
$share = $this->shareManager->newShare();
|
||||
$share->setNode($target);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:loading="isLoading || !!isSearching"
|
||||
:disabled="isLoading"
|
||||
track-by="multiselectKey"
|
||||
:internal-search="true"
|
||||
:internal-search="false"
|
||||
@input="clickAddAcl"
|
||||
@search-change="asyncFind">
|
||||
<template #noOptions>
|
||||
@@ -73,6 +73,7 @@ import { CollectionList } from 'nextcloud-vue-collections'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'SharingTabSidebar',
|
||||
@@ -148,18 +149,13 @@ export default {
|
||||
this.asyncFind('')
|
||||
},
|
||||
methods: {
|
||||
debouncedFind: debounce(async function(query) {
|
||||
this.isSearching = true
|
||||
await this.$store.dispatch('loadSharees', query)
|
||||
this.isSearching = false
|
||||
}, 300),
|
||||
async asyncFind(query) {
|
||||
// manual debounce to handle async searching more easily and have more control over the loading state
|
||||
const timestamp = (new Date()).getTime()
|
||||
if (!this.isSearching || timestamp > this.isSearching + 300) {
|
||||
this.isSearching = timestamp
|
||||
await this.$store.dispatch('loadSharees', query)
|
||||
|
||||
// only reset searching flag if the most recent search finished
|
||||
if (this.isSearching === timestamp) {
|
||||
this.isSearching = false
|
||||
}
|
||||
}
|
||||
await this.debouncedFind(query)
|
||||
},
|
||||
async clickAddAcl() {
|
||||
this.addAclForAPI = {
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
type="deck-card" />
|
||||
</div>
|
||||
|
||||
<Description :key="card.id" :card="card" />
|
||||
<Description :key="card.id" :card="card" @change="descriptionChanged" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -234,6 +234,9 @@ export default {
|
||||
this.initialize()
|
||||
},
|
||||
methods: {
|
||||
descriptionChanged(newDesc) {
|
||||
this.copiedCard.description = newDesc
|
||||
},
|
||||
async initialize() {
|
||||
if (!this.card) {
|
||||
return
|
||||
@@ -253,9 +256,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
setDue() {
|
||||
this.$store.dispatch('updateCardDue', this.copiedCard)
|
||||
},
|
||||
removeDue() {
|
||||
this.copiedCard.duedate = null
|
||||
this.$store.dispatch('updateCardDue', this.copiedCard)
|
||||
|
||||
@@ -218,6 +218,7 @@ export default {
|
||||
await this.$store.dispatch('updateCardDesc', { ...this.card, description: this.description })
|
||||
this.descriptionLastEdit = 0
|
||||
this.descriptionSaving = false
|
||||
this.$emit('change', this.description)
|
||||
},
|
||||
updateDescription() {
|
||||
this.descriptionLastEdit = Date.now()
|
||||
|
||||
@@ -235,9 +235,7 @@ export default {
|
||||
try {
|
||||
const newBoard = await this.$store.dispatch('cloneBoard', this.board)
|
||||
this.loading = false
|
||||
const route = this.routeTo
|
||||
route.params.id = newBoard.id
|
||||
this.$router.push(route)
|
||||
this.$router.push({ name: 'board', params: { id: newBoard.id } })
|
||||
} catch (e) {
|
||||
OC.Notification.showTemporary(t('deck', 'An error occurred'))
|
||||
console.error(e)
|
||||
@@ -278,9 +276,7 @@ export default {
|
||||
)
|
||||
},
|
||||
actionDetails() {
|
||||
const route = this.routeTo
|
||||
route.name = 'board.details'
|
||||
this.$router.push(route)
|
||||
this.$router.push({ name: 'board.details', params: { id: this.routeTo.id } })
|
||||
},
|
||||
applyEdit(e) {
|
||||
this.editing = false
|
||||
@@ -298,11 +294,6 @@ export default {
|
||||
cancelEdit(e) {
|
||||
this.editing = false
|
||||
},
|
||||
showSidebar() {
|
||||
const route = this.routeTo
|
||||
route.name = 'board.details'
|
||||
this.$router.push(route)
|
||||
},
|
||||
async updateSetting(key, value) {
|
||||
this.updateDueSetting = value
|
||||
const setting = {}
|
||||
|
||||
@@ -416,7 +416,7 @@ export default new Vuex.Store({
|
||||
params.append('search', query)
|
||||
params.append('format', 'json')
|
||||
params.append('perPage', 20)
|
||||
params.append('itemType', [0, 1, 7])
|
||||
params.append('itemType', [0, 1, 4, 7])
|
||||
|
||||
const response = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', { params })
|
||||
commit('setSharees', response.data.ocs.data)
|
||||
|
||||
@@ -25,10 +25,10 @@ namespace OCA\Deck\Service;
|
||||
|
||||
use OCA\Deck\Db\Attachment;
|
||||
use OCA\Deck\Db\AttachmentMapper;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\StreamResponse;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\IMimeTypeDetector;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\SimpleFS\ISimpleFile;
|
||||
use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
@@ -57,6 +57,8 @@ class FileServiceTest extends TestCase {
|
||||
private $config;
|
||||
/** @var AttachmentMapper|MockObject */
|
||||
private $attachmentMapper;
|
||||
/** @var IMimeTypeDetector|MockObject */
|
||||
private $mimeTypeDetector;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
@@ -67,7 +69,8 @@ class FileServiceTest extends TestCase {
|
||||
$this->rootFolder = $this->createMock(IRootFolder::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
|
||||
$this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger, $this->rootFolder, $this->config, $this->attachmentMapper);
|
||||
$this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class);
|
||||
$this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger, $this->rootFolder, $this->config, $this->attachmentMapper, $this->mimeTypeDetector);
|
||||
}
|
||||
|
||||
public function mockGetFolder($cardId) {
|
||||
@@ -268,51 +271,13 @@ class FileServiceTest extends TestCase {
|
||||
$file->expects($this->any())
|
||||
->method('fopen')
|
||||
->willReturn('fileresource');
|
||||
$this->mimeTypeDetector->expects($this->once())
|
||||
->method('getSecureMimeType')
|
||||
->willReturn('image/jpeg');
|
||||
$actual = $this->fileService->display($attachment);
|
||||
$expected = new StreamResponse('fileresource');
|
||||
$expected->addHeader('Content-Type', 'image/jpeg');
|
||||
$expected->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
|
||||
$policy = new ContentSecurityPolicy();
|
||||
$policy->addAllowedObjectDomain('\'self\'');
|
||||
$policy->addAllowedObjectDomain('blob:');
|
||||
$policy->addAllowedMediaDomain('\'self\'');
|
||||
$policy->addAllowedMediaDomain('blob:');
|
||||
$expected->setContentSecurityPolicy($policy);
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testDisplayPdf() {
|
||||
$this->config->expects($this->once())
|
||||
->method('getSystemValue')
|
||||
->willReturn('123');
|
||||
$appDataFolder = $this->createMock(Folder::class);
|
||||
$deckAppDataFolder = $this->createMock(Folder::class);
|
||||
$cardFolder = $this->createMock(Folder::class);
|
||||
$this->rootFolder->expects($this->once())->method('get')->willReturn($appDataFolder);
|
||||
$appDataFolder->expects($this->once())->method('get')->willReturn($deckAppDataFolder);
|
||||
$deckAppDataFolder->expects($this->once())->method('get')->willReturn($cardFolder);
|
||||
$attachment = $this->getAttachment();
|
||||
$file = $this->createMock(\OCP\Files\File::class);
|
||||
$cardFolder->expects($this->once())->method('get')->willReturn($file);
|
||||
$file->expects($this->any())
|
||||
->method('getMimeType')
|
||||
->willReturn('application/pdf');
|
||||
$file->expects($this->any())
|
||||
->method('getName')
|
||||
->willReturn('file1');
|
||||
$file->expects($this->any())
|
||||
->method('fopen')
|
||||
->willReturn('fileresource');
|
||||
$actual = $this->fileService->display($attachment);
|
||||
$expected = new StreamResponse('fileresource');
|
||||
$expected->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
|
||||
$expected->addHeader('Content-Type', 'application/pdf');
|
||||
$policy = new ContentSecurityPolicy();
|
||||
$policy->addAllowedObjectDomain('\'self\'');
|
||||
$policy->addAllowedObjectDomain('blob:');
|
||||
$policy->addAllowedMediaDomain('\'self\'');
|
||||
$policy->addAllowedMediaDomain('blob:');
|
||||
$expected->setContentSecurityPolicy($policy);
|
||||
$expected->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user