Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
This commit is contained in:
Julien Veyssier 2021-10-20 12:18:17 +02:00
Родитель 477573d97a
Коммит b7234c0401
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4141FEE162030638
4 изменённых файлов: 289 добавлений и 1 удалений

Просмотреть файл

@ -27,6 +27,8 @@ namespace OCA\Text\AppInfo;
return [
'routes' => [
['name' => 'Image#downloadImageLink', 'url' => '/image/link', 'verb' => 'POST'],
['name' => 'Session#create', 'url' => '/session/create', 'verb' => 'PUT'],
['name' => 'Session#fetch', 'url' => '/session/fetch', 'verb' => 'POST'],
['name' => 'Session#sync', 'url' => '/session/sync', 'verb' => 'POST'],

Просмотреть файл

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Julien Veyssier <eneiluj@posteo.net>
*
* @author Julien Veyssier <eneiluj@posteo.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Text\Controller;
use OCP\AppFramework\Http;
use OCA\Text\Service\ImageService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
class ImageController extends Controller {
/**
* @var string|null
*/
private $userId;
/**
* @var ImageService
*/
private $imageService;
public function __construct(string $appName,
IRequest $request,
ImageService $imageService,
?string $userId) {
parent::__construct($appName, $request);
$this->userId = $userId;
$this->imageService = $imageService;
}
/**
* @NoAdminRequired
*/
public function downloadImageLink(string $link): DataResponse {
$downloadResult = $this->imageService->downloadImageLink($link, $this->userId);
if (isset($downloadResult['error'])) {
return new DataResponse($downloadResult, Http::STATUS_BAD_REQUEST);
} else {
return new DataResponse($downloadResult);
}
}
}

Просмотреть файл

@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Julien Veyssier <eneiluj@posteo.net>
*
* @author Julien Veyssier <eneiluj@posteo.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Text\Service;
use Exception;
use OCP\Files\Node;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use Throwable;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use OCA\Text\AppInfo\Application;
use OCP\Http\Client\IClientService;
use OCP\Files\IRootFolder;
use Psr\Log\LoggerInterface;
use OCP\Share\IManager as ShareManager;
use function json_encode;
use function preg_replace;
class ImageService {
/**
* @var string|null
*/
private $userId;
/**
* @var ShareManager
*/
private $shareManager;
/**
* @var IRootFolder
*/
private $rootFolder;
/**
* @var IClientService
*/
private $clientService;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(IRootFolder $rootFolder,
LoggerInterface $logger,
ShareManager $shareManager,
IClientService $clientService) {
$this->rootFolder = $rootFolder;
$this->shareManager = $shareManager;
$this->clientService = $clientService;
$this->logger = $logger;
}
/**
* @param string $link
* @return array
*/
public function downloadImageLink(string $link, string $userId): array {
$fileName = (string) time();
$saveDir = $this->getOrCreateTextDirectory($userId);
$savedFile = $saveDir->newFile($fileName);
$resource = $savedFile->fopen('w');
$res = $this->simpleDownload($link, $resource);
if (is_resource($resource)) {
fclose($resource);
}
$savedFile->touch();
if (isset($res['Content-Type'])) {
if ($res['Content-Type'] === 'image/jpg') {
$fileName = $fileName . '.jpg';
} elseif ($res['Content-Type'] === 'image/png') {
$fileName = $fileName . '.png';
} else {
return [
'error' => 'Unsupported file type',
];
}
$targetPath = $saveDir->getPath() . '/' . $fileName;
$savedFile->move($targetPath);
$path = preg_replace('/^files/', '', $savedFile->getInternalPath());
// get file type and name
return [
'name' => $fileName,
'path' => $path,
];
} else {
return $res;
}
}
private function getOrCreateTextDirectory(string $userId): ?Node {
$userFolder = $this->rootFolder->getUserFolder($userId);
if ($userFolder->nodeExists('/Text')) {
$node = $userFolder->get('Text');
if ($node->getType() === FileInfo::TYPE_FOLDER) {
return $node;
} else {
return null;
}
} else {
return $userFolder->newFolder('/Text');
}
}
private function simpleDownload(string $url, $resource, array $params = [], string $method = 'GET'): array {
$client = $this->clientService->newClient();
try {
$options = [
// does not work with sink if SSE is enabled
// 'sink' => $resource,
// rather use stream and write to the file ourselves
'stream' => true,
'timeout' => 0,
'headers' => [
'User-Agent' => 'Nextcloud Text',
],
];
if (count($params) > 0) {
if ($method === 'GET') {
$paramsContent = http_build_query($params);
$url .= '?' . $paramsContent;
} else {
$options['body'] = json_encode($params);
}
}
if ($method === 'GET') {
$response = $client->get($url, $options);
} else if ($method === 'POST') {
$response = $client->post($url, $options);
} else if ($method === 'PUT') {
$response = $client->put($url, $options);
} else if ($method === 'DELETE') {
$response = $client->delete($url, $options);
} else {
return ['error' => 'Bad HTTP method'];
}
$respCode = $response->getStatusCode();
$body = $response->getBody();
while (!feof($body)) {
// write ~5 MB chunks
$chunk = fread($body, 5000000);
fwrite($resource, $chunk);
}
return ['Content-Type' => $response->getHeader('Content-Type')];
} catch (ServerException | ClientException $e) {
//$response = $e->getResponse();
//if ($response->getStatusCode() === 401) {
$this->logger->warning('Impossible to download image: '.$e->getMessage(), ['app' => Application::APP_NAME]);
return ['error' => $e->getMessage()];
} catch (ConnectException $e) {
$this->logger->error('Connection error: ' . $e->getMessage(), ['app' => Application::APP_NAME]);
return ['error' => 'Connection error: ' . $e->getMessage()];
} catch (Throwable | Exception $e) {
return ['error' => 'Unknown error: ' . $e->getMessage()];
}
}
}

Просмотреть файл

@ -29,7 +29,7 @@
accept="image/*"
aria-hidden="true"
class="hidden-visually"
@change="onImageFilePicked" />
@change="onImageFilePicked">
<div v-if="isRichEditor" ref="menubar" class="menubar-icons">
<template v-for="(icon, $index) in allIcons">
<EmojiPicker v-if="icon.class === 'icon-emoji'"
@ -43,6 +43,7 @@
</EmojiPicker>
<Actions v-else-if="icon.class === 'icon-image'"
:key="icon.label"
ref="imageActions"
:default-icon="'icon-image'">
<button slot="icon"
class="icon-image"
@ -61,6 +62,17 @@
@click="onUploadImage(commands.image)">
{{ t('text', 'Upload file') }}
</ActionButton>
<ActionButton v-show="!showImageLinkPrompt"
icon="icon-link"
:close-after-click="false"
@click="showImageLinkPrompt = true">
{{ t('text', 'From link') }}
</ActionButton>
<ActionInput v-show="showImageLinkPrompt"
icon="icon-link"
@submit="onImageLinksubmit($event, commands.image)">
{{ t('text', 'Image link') }}
</ActionInput>
</Actions>
<button v-else-if="icon.class"
v-show="$index < iconCount"
@ -121,16 +133,20 @@ import isMobile from './../mixins/isMobile'
import Actions from '@nextcloud/vue/dist/Components/Actions'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import ActionInput from '@nextcloud/vue/dist/Components/ActionInput'
import PopoverMenu from '@nextcloud/vue/dist/Components/PopoverMenu'
import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker'
import ClickOutside from 'vue-click-outside'
import { getCurrentUser } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
export default {
name: 'MenuBar',
components: {
EditorMenuBar,
ActionButton,
ActionInput,
PopoverMenu,
Actions,
EmojiPicker,
@ -173,6 +189,7 @@ export default {
forceRecompute: 0,
submenuVisibility: {},
lastImagePath: null,
showImageLinkPrompt: false,
icons: [...menuBarIcons],
}
},
@ -366,6 +383,24 @@ export default {
this.imageCommand = null
})
},
onImageLinksubmit(event, command) {
this.showImageLinkPrompt = false
const link = event.target[1].value
this.$refs.imageActions[0].closeMenu()
const params = {
link,
}
const url = generateUrl('/apps/text/image/link')
axios.post(url, params).then((response) => {
console.debug('link success', response.data)
this.insertImage(response.data?.path, command)
}).catch((error) => {
console.debug('link error', error)
}).then(() => {
})
},
showImagePrompt(command) {
const currentUser = getCurrentUser()
if (!currentUser) {