From 2aaa1bbdb54f7aade1f43b7e4ec8992e2d8da36b Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Thu, 5 Oct 2017 13:08:27 +0200 Subject: [PATCH 01/10] Add endpoint for public editor previews Signed-off-by: Roeland Jago Douma --- appinfo/routes.php | 5 + .../PublicFileHandlingController.php | 126 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 lib/Controller/PublicFileHandlingController.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 633908a5..0fe9bbb9 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -31,6 +31,11 @@ 'name' => 'FileHandling#save', 'url' => '/ajax/savefile', 'verb' => 'PUT' + ], + [ + 'name' => 'PublicFileHandling#load', + 'url' => '/public/{token}', + 'verb' => 'GET' ] ] ]; diff --git a/lib/Controller/PublicFileHandlingController.php b/lib/Controller/PublicFileHandlingController.php new file mode 100644 index 00000000..e9fca5d3 --- /dev/null +++ b/lib/Controller/PublicFileHandlingController.php @@ -0,0 +1,126 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ +namespace OCA\FilesTextEditor\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\Files\Folder; +use OCP\Files\NotFoundException; +use OCP\IL10N; +use OCP\IRequest; +use OCP\ISession; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager as ShareManager; + +class PublicFileHandlingController extends Controller{ + + /** @var IL10N */ + private $l; + + /** @var ShareManager */ + private $shareManager; + + /** @var ISession */ + private $session; + + /** + * + * @param string $AppName + * @param IRequest $request + * @param IL10N $l10n + * @param ShareManager $shareManager + * @param ISession $session + */ + public function __construct($AppName, + IRequest $request, + IL10N $l10n, + ShareManager $shareManager, + ISession $session) { + parent::__construct($AppName, $request); + $this->l = $l10n; + $this->shareManager = $shareManager; + $this->session = $session; + } + + /** + * load text file + * + * @NoAdminRequired + * @PublicPage + * @NoCSRFRequired + * + * @param string $token + * @return DataResponse + */ + public function load($token) { + try { + $share = $this->shareManager->getShareByToken($token); + } catch (ShareNotFound $e) { + return new DataResponse(['message' => $this->l->t('Share not found'), Http::STATUS_NOT_FOUND]); + } + + if ($share->getPassword() !== null && + (!$this->session->exists('public_link_authenticated') + || $this->session->get('public_link_authenticated') !== (string)$share->getId())) { + return new DataResponse(['message' => $this->l->t('You are not authorized to open this share'), Http::STATUS_BAD_REQUEST]); + } + + try { + $node = $share->getNode(); + } catch (NotFoundException $e) { + return new DataResponse(['message' => $this->l->t('Share not found'), Http::STATUS_NOT_FOUND]); + } + + if ($node instanceof Folder) { + return new DataResponse(['message' => $this->l->t('You can not open a folder')], Http::STATUS_BAD_REQUEST); + } + + // default of 4MB + $maxSize = 4194304; + if ($node->getSize() > $maxSize) { + return new DataResponse(['message' => $this->l->t('This file is too big to be opened. Please download the file instead.')], Http::STATUS_BAD_REQUEST); + } + + $fileContents = $node->getContent(); + if ($fileContents !== false) { + $encoding = mb_detect_encoding($fileContents . 'a', 'UTF-8, WINDOWS-1252, ISO-8859-15, ISO-8859-1, ASCII', true); + if ($encoding === '') { + // set default encoding if it couldn't be detected + $encoding = 'ISO-8859-15'; + } + $fileContents = iconv($encoding, 'UTF-8', $fileContents); + return new DataResponse( + [ + 'filecontents' => $fileContents, + 'writeable' => false, + 'mime' => $node->getMimeType(), + 'mtime' => $node->getMTime(), + ], + Http::STATUS_OK + ); + } + + return new DataResponse(['message' => $this->l->t('Cannot read the file.')], Http::STATUS_BAD_REQUEST); + } +} From 2fe8af932917c363f1d14954d0fa8c0fe7bbe3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 9 Feb 2018 09:29:27 +0100 Subject: [PATCH 02/10] Move "public-share.css" to "public-share.scss" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- css/{public-share.css => public-share.scss} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename css/{public-share.css => public-share.scss} (100%) diff --git a/css/public-share.css b/css/public-share.scss similarity index 100% rename from css/public-share.css rename to css/public-share.scss From 70e94722ec1815ab990891a3162d90a39e735d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 9 Feb 2018 09:43:05 +0100 Subject: [PATCH 03/10] Move (S)CSS rules for ".preview" descendants into ".preview" rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- css/public-share.scss | 122 +++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/css/public-share.scss b/css/public-share.scss index 7b1c83d1..d21e66db 100644 --- a/css/public-share.scss +++ b/css/public-share.scss @@ -8,70 +8,76 @@ color: #333; text-align: left; margin-bottom: 170px !important; -} -.preview h1, -.preview h2, -.preview h3 { - margin-top: 1em; - word-wrap: break-word; -} + h1, + h2, + h3 { + margin-top: 1em; + word-wrap: break-word; + } -.preview h1 { - line-height: 1.1em; - margin-bottom: 1em; - text-align: center; - font-size: 3em; -} + h1 { + line-height: 1.1em; + margin-bottom: 1em; + text-align: center; + font-size: 3em; + } -.preview h2 { - line-height: 1.1em; - margin-bottom: .5em; - font-size: 2em; -} + h2 { + line-height: 1.1em; + margin-bottom: .5em; + font-size: 2em; + } -.preview h3 { - line-height: 1.1em; - margin-bottom: .6em; - font-size: 1.4em; -} + h3 { + line-height: 1.1em; + margin-bottom: .6em; + font-size: 1.4em; + } -.preview p, -.preview ul, -.preview ol, -.preview pre { - line-height: 1.45em; -} -.preview p, -.preview pre { - margin-bottom: 17.6px; -} -.preview ul, -.preview ol { - padding-left: 25px; -} + p, + ul, + ol, + pre { + line-height: 1.45em; + } + p, + pre { + margin-bottom: 17.6px; + } + ul, + ol { + padding-left: 25px; + } -.preview ul { - list-style-type: disc; -} + ul { + list-style-type: disc; + } -.preview ul ul { - list-style-type: circle; -} + ul ul { + list-style-type: circle; + } -.preview a { - color: #448ac9; - text-decoration: none; -} -.preview a:hover, -.preview a:focus, -.preview a:active { - text-decoration: underline; -} + a { + color: #448ac9; + text-decoration: none; + } + a:hover, + a:focus, + a:active { + text-decoration: underline; + } -.preview em { - opacity: 1; - font-style: italic; + em { + opacity: 1; + font-style: italic; + } + + code { + background-color: rgb(242, 242, 242); + border-radius: 3px; + padding: 0px 2px 2px 2px; + } } /* hide image preview during loading or if it renders fine */ @@ -79,9 +85,3 @@ .preview .text-preview { display: none !important; } - -.preview code { - background-color: rgb(242, 242, 242); - border-radius: 3px; - padding: 0px 2px 2px 2px; -} From 6f351e10e0322cfe09ff313b38d6ee7ec524b3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 9 Feb 2018 09:51:23 +0100 Subject: [PATCH 04/10] Use CSS rules for formatted text only with formatted text previews MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will be needed in a following commit when plain text previews generated by this app are introduced. Signed-off-by: Daniel Calviño Sánchez --- css/public-share.scss | 2 +- js/public-share.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/css/public-share.scss b/css/public-share.scss index d21e66db..393f7c8c 100644 --- a/css/public-share.scss +++ b/css/public-share.scss @@ -1,4 +1,4 @@ -.preview { +.preview.formatted-text { max-width: 810px; padding: 0px 20px !important; box-sizing: border-box; diff --git a/js/public-share.js b/js/public-share.js index 7210ded9..c1302db0 100644 --- a/js/public-share.js +++ b/js/public-share.js @@ -45,7 +45,7 @@ $(document).ready(function(){ $.get(downloadUrl).success(function(content) { previewElement .removeClass('icon-loading') - .addClass('preview') + .addClass('preview formatted-text') .html(DOMPurify.sanitize( marked(content, { renderer: renderer, From f107a39724828c4b8ee876ffd1649ee474698a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 9 Feb 2018 10:22:11 +0100 Subject: [PATCH 05/10] Remove children elements instead of detaching them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The children elements are not attached again later, so they can be removed instead of just detached. Signed-off-by: Daniel Calviño Sánchez --- js/public-share.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/public-share.js b/js/public-share.js index c1302db0..b0775d88 100644 --- a/js/public-share.js +++ b/js/public-share.js @@ -40,7 +40,7 @@ $(document).ready(function(){ previewElement .addClass('icon-loading') - .children().detach(); + .children().remove(); $.get(downloadUrl).success(function(content) { previewElement From 4060462b426bb5186308a096540935df007094db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 9 Feb 2018 10:28:06 +0100 Subject: [PATCH 06/10] Add plain text previews MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default plain text previews always assume that the text file uses UTF-8 encoding, so when it uses a different one the characters are not properly parsed by the browser. The "public" endpoint from this app tries to guess the actual encoding of the file and then converts the data sent to the browser to UTF-8. Thus, now the default plain text previews are overriden with the preview generated by this app. Note that this does not prevent the default plain text preview from being generated, though; it is simply hidden. Following the same approach used for Markdown previews, ".preview" is added to the "#imgframe" element, which is the parent element for the default plain text preview. This removes the default plain text preview thanks to the CSS rules. However, as the "overrider" plain text preview is also a descendant of ".preview", a special CSS class has to be added to the "overrider" plain text preview to prevent it from being hidden. Signed-off-by: Daniel Calviño Sánchez --- css/public-share.scss | 2 +- js/public-share.js | 49 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/css/public-share.scss b/css/public-share.scss index 393f7c8c..c5d9ad72 100644 --- a/css/public-share.scss +++ b/css/public-share.scss @@ -82,6 +82,6 @@ /* hide image preview during loading or if it renders fine */ .icon-loading .text-preview, -.preview .text-preview { +.preview .text-preview:not(.default-overridden) { display: none !important; } diff --git a/js/public-share.js b/js/public-share.js index b0775d88..eb00eb07 100644 --- a/js/public-share.js +++ b/js/public-share.js @@ -1,8 +1,12 @@ // FIXME: Hack for single public file view since it is not attached to the fileslist $(document).ready(function(){ - if ($('#isPublic').val() && - $('#mimetype').val() === 'text/markdown' && - $('#filesize').val() < 524288) { + var isPublic = $('#isPublic').val(); + var mimetype = $('#mimetype').val(); + var filesize = $('#filesize').val(); + + if (isPublic && + mimetype === 'text/markdown' && + filesize < 524288) { var sharingToken = $('#sharingToken').val(); var downloadUrl = OC.generateUrl('/s/{token}/download', {token: sharingToken}); @@ -64,5 +68,44 @@ $(document).ready(function(){ previewElement .removeClass('icon-loading'); }); + } else if (isPublic && + mimetype.substr(0, mimetype.indexOf('/')) === 'text') { + // Based on default text previews from "files_sharing/js/public.js", but + // using the public endpoint from files_texteditor for better character + // encoding support. + var previewElement = $('#imgframe'); + previewElement + .addClass('icon-loading') + .children().remove(); + + var bottomMargin = 350; + var previewHeight = $(window).height() - bottomMargin; + previewHeight = Math.max(200, previewHeight); + + var sharingToken = $('#sharingToken').val(); + $.ajax({ + url: OC.generateUrl('/apps/files_texteditor/public/{token}', { token: sharingToken }), + headers: { + 'Range': 'bytes=0-1000' + } + }).success(function(content) { + var textDiv = $('
').addClass('text-preview default-overridden'); + textDiv.text(content.filecontents); + + previewElement + .removeClass('icon-loading') + .addClass('preview') + .append(textDiv); + + var divHeight = textDiv.height(); + if (content.filecontents.length > 999) { + var ellipsis = $('
').addClass('ellipsis'); + ellipsis.html('(…)'); + ellipsis.appendTo('#imgframe'); + } + if (divHeight > previewHeight) { + textDiv.height(previewHeight); + } + }); } }); From 95c88677ef03f29607450ed02204c26f505ca251 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Fri, 9 Feb 2018 13:41:57 +0100 Subject: [PATCH 07/10] Parse range request Signed-off-by: Roeland Jago Douma --- lib/Controller/PublicFileHandlingController.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/Controller/PublicFileHandlingController.php b/lib/Controller/PublicFileHandlingController.php index e9fca5d3..d5b1db52 100644 --- a/lib/Controller/PublicFileHandlingController.php +++ b/lib/Controller/PublicFileHandlingController.php @@ -96,6 +96,17 @@ public function load($token) { return new DataResponse(['message' => $this->l->t('You can not open a folder')], Http::STATUS_BAD_REQUEST); } + $range = $this->request->getHeader('Range'); + if ($range !== '') { + $matches = []; + if (preg_match('/bytes=0-(\d+)$/', $range, $matches) === 0) { + return new DataResponse(['message' => $this->l->t('Invalid range request')], Http::STATUS_REQUEST_RANGE_NOT_SATISFIABLE); + } + $range = (int)$matches[1]; + } else { + $range = -1; + } + // default of 4MB $maxSize = 4194304; if ($node->getSize() > $maxSize) { @@ -110,6 +121,11 @@ public function load($token) { $encoding = 'ISO-8859-15'; } $fileContents = iconv($encoding, 'UTF-8', $fileContents); + + if ($range !== -1) { + $fileContents = mb_substr($fileContents, 0, $range); + } + return new DataResponse( [ 'filecontents' => $fileContents, From 9e2a6867b0578e8b106166ebe4506c9a3817eae5 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Thu, 8 Mar 2018 14:19:19 +0100 Subject: [PATCH 08/10] Don't do do json just return a blob of text * Fix JS * Request more data Signed-off-by: Roeland Jago Douma --- js/public-share.js | 6 +++--- lib/Controller/PublicFileHandlingController.php | 7 +------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/js/public-share.js b/js/public-share.js index eb00eb07..28edf39f 100644 --- a/js/public-share.js +++ b/js/public-share.js @@ -86,11 +86,11 @@ $(document).ready(function(){ $.ajax({ url: OC.generateUrl('/apps/files_texteditor/public/{token}', { token: sharingToken }), headers: { - 'Range': 'bytes=0-1000' + 'Range': 'bytes=0-524288' } }).success(function(content) { var textDiv = $('
').addClass('text-preview default-overridden'); - textDiv.text(content.filecontents); + textDiv.text(content); previewElement .removeClass('icon-loading') @@ -98,7 +98,7 @@ $(document).ready(function(){ .append(textDiv); var divHeight = textDiv.height(); - if (content.filecontents.length > 999) { + if (content.length > 50000) { var ellipsis = $('
').addClass('ellipsis'); ellipsis.html('(…)'); ellipsis.appendTo('#imgframe'); diff --git a/lib/Controller/PublicFileHandlingController.php b/lib/Controller/PublicFileHandlingController.php index d5b1db52..42a38bdd 100644 --- a/lib/Controller/PublicFileHandlingController.php +++ b/lib/Controller/PublicFileHandlingController.php @@ -127,12 +127,7 @@ public function load($token) { } return new DataResponse( - [ - 'filecontents' => $fileContents, - 'writeable' => false, - 'mime' => $node->getMimeType(), - 'mtime' => $node->getMTime(), - ], + $fileContents, Http::STATUS_OK ); } From 5be86b5e6141c4b819b2f776938f5f2cb1ecece1 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Fri, 9 Mar 2018 14:05:49 +0100 Subject: [PATCH 09/10] Set Content-Type Signed-off-by: Roeland Jago Douma --- lib/Controller/PublicFileHandlingController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Controller/PublicFileHandlingController.php b/lib/Controller/PublicFileHandlingController.php index 42a38bdd..64518451 100644 --- a/lib/Controller/PublicFileHandlingController.php +++ b/lib/Controller/PublicFileHandlingController.php @@ -128,7 +128,8 @@ public function load($token) { return new DataResponse( $fileContents, - Http::STATUS_OK + Http::STATUS_OK, + ['Content-Type' => 'text/plain; charset="utf-8"'] ); } From 8ab0443f67a4732b2a1a6dcacb991c18c2eb884c Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Thu, 12 Apr 2018 23:41:21 +0200 Subject: [PATCH 10/10] Use proper response Signed-off-by: Roeland Jago Douma --- lib/Controller/PublicFileHandlingController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Controller/PublicFileHandlingController.php b/lib/Controller/PublicFileHandlingController.php index 64518451..eca32b7a 100644 --- a/lib/Controller/PublicFileHandlingController.php +++ b/lib/Controller/PublicFileHandlingController.php @@ -126,7 +126,7 @@ public function load($token) { $fileContents = mb_substr($fileContents, 0, $range); } - return new DataResponse( + return new Http\DataDisplayResponse( $fileContents, Http::STATUS_OK, ['Content-Type' => 'text/plain; charset="utf-8"']