From 3f3591bae0a458f4c66fccb9f239a2e32bba442d Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 25 Nov 2021 19:24:32 +0100 Subject: [PATCH] web client: allow to preview images and pdf pdf depends on browser support. It does not work on mobile devices. --- httpd/api_http_user.go | 3 +- httpd/api_utils.go | 8 +++- httpd/httpd.go | 5 ++- httpd/httpd_test.go | 28 ++++++++++++- httpd/server.go | 4 +- httpd/webclient.go | 39 ++++++++++++++++-- openapi/openapi.yaml | 6 +++ .../codemirror/addon/search/searchcursor.js | 31 +++++++++----- static/vendor/codemirror/codemirror.css | 20 ++++----- static/vendor/codemirror/codemirror.js | 36 +++++++++------- static/vendor/lightbox2/css/lightbox.min.css | 1 + static/vendor/lightbox2/images/close.png | Bin 0 -> 280 bytes static/vendor/lightbox2/images/loading.gif | Bin 0 -> 8476 bytes static/vendor/lightbox2/images/next.png | Bin 0 -> 1350 bytes static/vendor/lightbox2/images/prev.png | Bin 0 -> 1360 bytes static/vendor/lightbox2/js/lightbox.min.js | 15 +++++++ static/vendor/pdfobject/pdfobject.min.js | 9 ++++ templates/webclient/editfile.html | 7 ++++ templates/webclient/files.html | 35 +++++++++++++--- templates/webclient/viewpdf.html | 15 +++++++ 20 files changed, 208 insertions(+), 54 deletions(-) create mode 100644 static/vendor/lightbox2/css/lightbox.min.css create mode 100644 static/vendor/lightbox2/images/close.png create mode 100644 static/vendor/lightbox2/images/loading.gif create mode 100644 static/vendor/lightbox2/images/next.png create mode 100644 static/vendor/lightbox2/images/prev.png create mode 100644 static/vendor/lightbox2/js/lightbox.min.js create mode 100644 static/vendor/pdfobject/pdfobject.min.js create mode 100644 templates/webclient/viewpdf.html diff --git a/httpd/api_http_user.go b/httpd/api_http_user.go index d574b6ef..de8c042a 100644 --- a/httpd/api_http_user.go +++ b/httpd/api_http_user.go @@ -155,7 +155,8 @@ func getUserFile(w http.ResponseWriter, r *http.Request) { return } - if status, err := downloadFile(w, r, connection, name, info); err != nil { + inline := r.URL.Query().Get("inline") != "" + if status, err := downloadFile(w, r, connection, name, info, inline); err != nil { resp := apiResponse{ Error: err.Error(), Message: http.StatusText(status), diff --git a/httpd/api_utils.go b/httpd/api_utils.go index 6997828f..1e20c7e4 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -257,7 +257,9 @@ func getZipEntryName(entryPath, baseDir string) string { return strings.TrimPrefix(entryPath, "/") } -func downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection, name string, info os.FileInfo) (int, error) { +func downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection, name string, + info os.FileInfo, inline bool, +) (int, error) { var err error rangeHeader := r.Header.Get("Range") if rangeHeader != "" && checkIfRange(r, info.ModTime()) == condFalse { @@ -295,7 +297,9 @@ func downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection } w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) w.Header().Set("Content-Type", ctype) - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%#v", path.Base(name))) + if !inline { + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%#v", path.Base(name))) + } w.Header().Set("Accept-Ranges", "bytes") w.WriteHeader(responseStatus) if r.Method != http.MethodHead { diff --git a/httpd/httpd.go b/httpd/httpd.go index 8dc36fbf..e63af524 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -136,13 +136,14 @@ const ( webClientPubSharesPathDefault = "/web/client/pubshares" webClientForgotPwdPathDefault = "/web/client/forgot-password" webClientResetPwdPathDefault = "/web/client/reset-password" + webClientViewPDFPathDefault = "/web/client/viewpdf" webStaticFilesPathDefault = "/static" webOpenAPIPathDefault = "/openapi" // MaxRestoreSize defines the max size for the loaddata input file MaxRestoreSize = 10485760 // 10 MB maxRequestSize = 1048576 // 1MB maxLoginBodySize = 262144 // 256 KB - httpdMaxEditFileSize = 524288 // 512 KB + httpdMaxEditFileSize = 1048576 // 1 MB maxMultipartMem = 8388608 // 8MB osWindows = "windows" otpHeaderCode = "X-SFTPGO-OTP" @@ -210,6 +211,7 @@ var ( webClientLogoutPath string webClientForgotPwdPath string webClientResetPwdPath string + webClientViewPDFPath string webStaticFilesPath string webOpenAPIPath string // max upload size for http clients, 1GB by default @@ -570,6 +572,7 @@ func updateWebClientURLs(baseURL string) { webClientRecoveryCodesPath = path.Join(baseURL, webClientRecoveryCodesPathDefault) webClientForgotPwdPath = path.Join(baseURL, webClientForgotPwdPathDefault) webClientResetPwdPath = path.Join(baseURL, webClientResetPwdPathDefault) + webClientViewPDFPath = path.Join(baseURL, webClientViewPDFPathDefault) } func updateWebAdminURLs(baseURL string) { diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index a39fc253..f9f39566 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -153,6 +153,7 @@ const ( webClientPubSharesPath = "/web/client/pubshares" webClientForgotPwdPath = "/web/client/forgot-password" webClientResetPwdPath = "/web/client/reset-password" + webClientViewPDFPath = "/web/client/viewpdf" httpBaseURL = "http://127.0.0.1:8081" sftpServerAddr = "127.0.0.1:8022" smtpServerAddr = "127.0.0.1:3525" @@ -9320,13 +9321,38 @@ func TestUserAPIKey(t *testing.T) { assert.NoError(t, err) } +func TestWebClientViewPDF(t *testing.T) { + user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) + assert.NoError(t, err) + + webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword) + assert.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, webClientViewPDFPath, nil) + assert.NoError(t, err) + setJWTCookieForReq(req, webToken) + rr := executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr) + + req, err = http.NewRequest(http.MethodGet, webClientViewPDFPath+"?path=test.pdf", nil) + assert.NoError(t, err) + setJWTCookieForReq(req, webToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) +} + func TestWebEditFile(t *testing.T) { user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) testFile1 := "testfile1.txt" testFile2 := "testfile2" file1Size := int64(65536) - file2Size := int64(655360) + file2Size := int64(1048576 * 2) err = createTestFile(filepath.Join(user.GetHomeDir(), testFile1), file1Size) assert.NoError(t, err) err = createTestFile(filepath.Join(user.GetHomeDir(), testFile2), file2Size) diff --git a/httpd/server.go b/httpd/server.go index 1785dc6b..88abc69b 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -1223,10 +1223,10 @@ func (s *httpdServer) initializeRouter() { router.Get(webClientLogoutPath, handleWebClientLogout) router.With(s.refreshCookie).Get(webClientFilesPath, handleClientGetFiles) + router.With(s.refreshCookie).Get(webClientViewPDFPath, handleClientViewPDF) router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader). Post(webClientFilesPath, uploadUserFiles) - router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.refreshCookie). - Get(webClientEditFilePath, handleClientEditFile) + router.With(s.refreshCookie).Get(webClientEditFilePath, handleClientEditFile) router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader). Patch(webClientFilesPath, renameUserFile) router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader). diff --git a/httpd/webclient.go b/httpd/webclient.go index 4180ea10..f430d1a6 100644 --- a/httpd/webclient.go +++ b/httpd/webclient.go @@ -44,6 +44,7 @@ const ( templateClientEditFile = "editfile.html" templateClientShare = "share.html" templateClientShares = "shares.html" + templateClientViewPDF = "viewpdf.html" pageClientFilesTitle = "My Files" pageClientSharesTitle = "Shares" pageClientProfileTitle = "My Profile" @@ -99,11 +100,18 @@ type dirMapping struct { Href string } +type viewPDFPage struct { + Title string + URL string + StaticURL string +} + type editFilePage struct { baseClientPage CurrentDir string Path string Name string + ReadOnly bool Data string } @@ -112,6 +120,7 @@ type filesPage struct { CurrentDir string DirsURL string DownloadURL string + ViewPDFURL string CanAddFiles bool CanCreateDirs bool CanRename bool @@ -229,6 +238,9 @@ func loadClientTemplates(templatesPath string) { resetPwdPaths := []string{ filepath.Join(templatesPath, templateCommonDir, templateResetPassword), } + viewPDFPaths := []string{ + filepath.Join(templatesPath, templateClientDir, templateClientViewPDF), + } filesTmpl := util.LoadTemplate(nil, filesPaths...) profileTmpl := util.LoadTemplate(nil, profilePaths...) @@ -243,6 +255,7 @@ func loadClientTemplates(templatesPath string) { shareTmpl := util.LoadTemplate(nil, sharePaths...) forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...) resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...) + viewPDFTmpl := util.LoadTemplate(nil, viewPDFPaths...) clientTemplates[templateClientFiles] = filesTmpl clientTemplates[templateClientProfile] = profileTmpl @@ -257,6 +270,7 @@ func loadClientTemplates(templatesPath string) { clientTemplates[templateClientShare] = shareTmpl clientTemplates[templateForgotPassword] = forgotPwdTmpl clientTemplates[templateResetPassword] = resetPwdTmpl + clientTemplates[templateClientViewPDF] = viewPDFTmpl } func getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage { @@ -391,12 +405,13 @@ func renderClientMFAPage(w http.ResponseWriter, r *http.Request) { renderClientTemplate(w, templateClientMFA, data) } -func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string) { +func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string, readOnly bool) { data := editFilePage{ baseClientPage: getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r), Path: fileName, Name: path.Base(fileName), CurrentDir: path.Dir(fileName), + ReadOnly: readOnly, Data: fileData, } @@ -427,6 +442,7 @@ func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error stri Error: error, CurrentDir: url.QueryEscape(dirName), DownloadURL: webClientDownloadZipPath, + ViewPDFURL: webClientViewPDFPath, DirsURL: webClientDirsPath, CanAddFiles: user.CanAddFilesFromWeb(dirName), CanCreateDirs: user.CanAddDirsFromWeb(dirName), @@ -650,7 +666,8 @@ func handleClientGetFiles(w http.ResponseWriter, r *http.Request) { renderFilesPage(w, r, name, "", user) return } - if status, err := downloadFile(w, r, connection, name, info); err != nil && status != 0 { + inline := r.URL.Query().Get("inline") != "" + if status, err := downloadFile(w, r, connection, name, info, inline); err != nil && status != 0 { if status > 0 { if status == http.StatusRequestedRangeNotSatisfiable { renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "") @@ -723,7 +740,7 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) { return } - renderEditFilePage(w, r, name, b.String()) + renderEditFilePage(w, r, name, b.String(), util.IsStringInSlice(sdk.WebClientWriteDisabled, user.Filters.WebClient)) } func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) { @@ -1035,3 +1052,19 @@ func handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) { } renderClientResetPwdPage(w, "") } + +func handleClientViewPDF(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize) + name := r.URL.Query().Get("path") + if name == "" { + renderClientBadRequestPage(w, r, errors.New("no file specified")) + return + } + name = util.CleanPath(name) + data := viewPDFPage{ + Title: path.Base(name), + URL: fmt.Sprintf("%v?path=%v&inline=1", webClientFilesPath, url.QueryEscape(name)), + StaticURL: webStaticFilesPath, + } + renderClientTemplate(w, templateClientViewPDF, data) +} diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index a395204b..64ea2d15 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -3546,6 +3546,12 @@ paths: description: Path to the file to download. It must be URL encoded, for example the path "my dir/àdir/file.txt" must be sent as "my%20dir%2F%C3%A0dir%2Ffile.txt" schema: type: string + - in: query + name: inline + required: false + description: 'If set, the response will not have the Content-Disposition header set to `attachment`' + schema: + type: string responses: '200': description: successful operation diff --git a/static/vendor/codemirror/addon/search/searchcursor.js b/static/vendor/codemirror/addon/search/searchcursor.js index d5869578..230017b7 100644 --- a/static/vendor/codemirror/addon/search/searchcursor.js +++ b/static/vendor/codemirror/addon/search/searchcursor.js @@ -202,6 +202,7 @@ function SearchCursor(doc, query, pos, options) { this.atOccurrence = false + this.afterEmptyMatch = false this.doc = doc pos = pos ? doc.clipPos(pos) : Pos(0, 0) this.pos = {from: pos, to: pos} @@ -237,21 +238,29 @@ findPrevious: function() {return this.find(true)}, find: function(reverse) { - var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to)) - - // Implements weird auto-growing behavior on null-matches for - // backwards-compatibility with the vim code (unfortunately) - while (result && CodeMirror.cmpPos(result.from, result.to) == 0) { + var head = this.doc.clipPos(reverse ? this.pos.from : this.pos.to); + if (this.afterEmptyMatch && this.atOccurrence) { + // do not return the same 0 width match twice + head = Pos(head.line, head.ch) if (reverse) { - if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1) - else if (result.from.line == this.doc.firstLine()) result = null - else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1))) + head.ch--; + if (head.ch < 0) { + head.line--; + head.ch = (this.doc.getLine(head.line) || "").length; + } } else { - if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1) - else if (result.to.line == this.doc.lastLine()) result = null - else result = this.matches(reverse, Pos(result.to.line + 1, 0)) + head.ch++; + if (head.ch > (this.doc.getLine(head.line) || "").length) { + head.ch = 0; + head.line++; + } + } + if (CodeMirror.cmpPos(head, this.doc.clipPos(head)) != 0) { + return this.atOccurrence = false } } + var result = this.matches(reverse, head) + this.afterEmptyMatch = result && CodeMirror.cmpPos(result.from, result.to) == 0 if (result) { this.pos = result diff --git a/static/vendor/codemirror/codemirror.css b/static/vendor/codemirror/codemirror.css index 86061bb0..d85fbc93 100644 --- a/static/vendor/codemirror/codemirror.css +++ b/static/vendor/codemirror/codemirror.css @@ -60,19 +60,13 @@ .cm-fat-cursor div.CodeMirror-cursors { z-index: 1; } -.cm-fat-cursor-mark { - background-color: rgba(20, 255, 20, 0.5); - -webkit-animation: blink 1.06s steps(1) infinite; - -moz-animation: blink 1.06s steps(1) infinite; - animation: blink 1.06s steps(1) infinite; -} -.cm-animate-fat-cursor { - width: auto; - -webkit-animation: blink 1.06s steps(1) infinite; - -moz-animation: blink 1.06s steps(1) infinite; - animation: blink 1.06s steps(1) infinite; - background-color: #7e7; -} +.cm-fat-cursor .CodeMirror-line::selection, +.cm-fat-cursor .CodeMirror-line > span::selection, +.cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; } +.cm-fat-cursor .CodeMirror-line::-moz-selection, +.cm-fat-cursor .CodeMirror-line > span::-moz-selection, +.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; } +.cm-fat-cursor { caret-color: transparent; } @-moz-keyframes blink { 0% {} 50% { background-color: transparent; } diff --git a/static/vendor/codemirror/codemirror.js b/static/vendor/codemirror/codemirror.js index d2e5bcc5..b9675f4b 100644 --- a/static/vendor/codemirror/codemirror.js +++ b/static/vendor/codemirror/codemirror.js @@ -2351,12 +2351,14 @@ function mapFromLineView(lineView, line, lineN) { if (lineView.line == line) { return {map: lineView.measure.map, cache: lineView.measure.cache} } - for (var i = 0; i < lineView.rest.length; i++) - { if (lineView.rest[i] == line) - { return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } } - for (var i$1 = 0; i$1 < lineView.rest.length; i$1++) - { if (lineNo(lineView.rest[i$1]) > lineN) - { return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } } + if (lineView.rest) { + for (var i = 0; i < lineView.rest.length; i++) + { if (lineView.rest[i] == line) + { return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } } + for (var i$1 = 0; i$1 < lineView.rest.length; i$1++) + { if (lineNo(lineView.rest[i$1]) > lineN) + { return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } } + } } // Render a line into the hidden node display.externalMeasured. Used @@ -3150,13 +3152,19 @@ var curFragment = result.cursors = document.createDocumentFragment(); var selFragment = result.selection = document.createDocumentFragment(); + var customCursor = cm.options.$customCursor; + if (customCursor) { primary = true; } for (var i = 0; i < doc.sel.ranges.length; i++) { if (!primary && i == doc.sel.primIndex) { continue } var range = doc.sel.ranges[i]; if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue } var collapsed = range.empty(); - if (collapsed || cm.options.showCursorWhenSelecting) - { drawSelectionCursor(cm, range.head, curFragment); } + if (customCursor) { + var head = customCursor(cm, range); + if (head) { drawSelectionCursor(cm, head, curFragment); } + } else if (collapsed || cm.options.showCursorWhenSelecting) { + drawSelectionCursor(cm, range.head, curFragment); + } if (!collapsed) { drawSelectionRange(cm, range, selFragment); } } @@ -3174,9 +3182,8 @@ if (/\bcm-fat-cursor\b/.test(cm.getWrapperElement().className)) { var charPos = charCoords(cm, head, "div", null, null); - if (charPos.right - charPos.left > 0) { - cursor.style.width = (charPos.right - charPos.left) + "px"; - } + var width = charPos.right - charPos.left; + cursor.style.width = (width > 0 ? width : cm.defaultCharWidth()) + "px"; } if (pos.other) { @@ -3649,6 +3656,7 @@ this.vert.firstChild.style.height = Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; } else { + this.vert.scrollTop = 0; this.vert.style.display = ""; this.vert.firstChild.style.height = "0"; } @@ -4501,7 +4509,7 @@ function onScrollWheel(cm, e) { var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; var pixelsPerUnit = wheelPixelsPerUnit; - if (event.deltaMode === 0) { + if (e.deltaMode === 0) { dx = e.deltaX; dy = e.deltaY; pixelsPerUnit = 1; @@ -8235,7 +8243,7 @@ } function hiddenTextarea() { - var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none"); + var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; min-height: 1em; outline: none"); var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); // The textarea is kept positioned near the cursor to prevent the // fact that it'll be scrolled into view on input from scrolling @@ -9832,7 +9840,7 @@ addLegacyProps(CodeMirror); - CodeMirror.version = "5.63.1"; + CodeMirror.version = "5.64.0"; return CodeMirror; diff --git a/static/vendor/lightbox2/css/lightbox.min.css b/static/vendor/lightbox2/css/lightbox.min.css new file mode 100644 index 00000000..adbaa837 --- /dev/null +++ b/static/vendor/lightbox2/css/lightbox.min.css @@ -0,0 +1 @@ +.lb-loader,.lightbox{text-align:center;line-height:0;position:absolute;left:0}body.lb-disable-scrolling{overflow:hidden}.lightboxOverlay{position:absolute;top:0;left:0;z-index:9999;background-color:#000;filter:alpha(Opacity=80);opacity:.8;display:none}.lightbox{width:100%;z-index:10000;font-weight:400;outline:0}.lightbox .lb-image{display:block;height:auto;max-width:inherit;max-height:none;border-radius:3px;border:4px solid #fff}.lightbox a img{border:none}.lb-outerContainer{position:relative;width:250px;height:250px;margin:0 auto;border-radius:4px;background-color:#fff}.lb-outerContainer:after{content:"";display:table;clear:both}.lb-loader{top:43%;height:25%;width:100%}.lb-cancel{display:block;width:32px;height:32px;margin:0 auto;background:url(../images/loading.gif) no-repeat}.lb-nav{position:absolute;top:0;left:0;height:100%;width:100%;z-index:10}.lb-container>.nav{left:0}.lb-nav a{outline:0;background-image:url(data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)}.lb-next,.lb-prev{height:100%;cursor:pointer;display:block}.lb-nav a.lb-prev{width:34%;left:0;float:left;background:url(../images/prev.png) left 48% no-repeat;filter:alpha(Opacity=0);opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-prev:hover{filter:alpha(Opacity=100);opacity:1}.lb-nav a.lb-next{width:64%;right:0;float:right;background:url(../images/next.png) right 48% no-repeat;filter:alpha(Opacity=0);opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-next:hover{filter:alpha(Opacity=100);opacity:1}.lb-dataContainer{margin:0 auto;padding-top:5px;width:100%;border-bottom-left-radius:4px;border-bottom-right-radius:4px}.lb-dataContainer:after{content:"";display:table;clear:both}.lb-data{padding:0 4px;color:#ccc}.lb-data .lb-details{width:85%;float:left;text-align:left;line-height:1.1em}.lb-data .lb-caption{font-size:13px;font-weight:700;line-height:1em}.lb-data .lb-caption a{color:#4ae}.lb-data .lb-number{display:block;clear:left;padding-bottom:1em;font-size:12px;color:#999}.lb-data .lb-close{display:block;float:right;width:30px;height:30px;background:url(../images/close.png) top right no-repeat;text-align:right;outline:0;filter:alpha(Opacity=70);opacity:.7;-webkit-transition:opacity .2s;-moz-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.lb-data .lb-close:hover{cursor:pointer;filter:alpha(Opacity=100);opacity:1} \ No newline at end of file diff --git a/static/vendor/lightbox2/images/close.png b/static/vendor/lightbox2/images/close.png new file mode 100644 index 0000000000000000000000000000000000000000..20baa1db5fdf9e22655a0f851c4401d36b9e7ef9 GIT binary patch literal 280 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i3?z4Pv7`ejn*g5>*Z=?j1DSvS{%vY%YG`Nx zvLV3R+Z)1I1qQoz?eg;S^6>Bg3hv&$d*{xbK#iaC4$A=f!X-g|!3vbDHnnh5Xg#OL;K8su!;fw4MC7hH<+BPou~5EnOggc)I$z JtaD0e0ssdsYEA$E literal 0 HcmV?d00001 diff --git a/static/vendor/lightbox2/images/loading.gif b/static/vendor/lightbox2/images/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..5087c2a644223a95984baefdb74f39a287fdfd9a GIT binary patch literal 8476 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 S-YhKu00000)){var b=this;a('
').appendTo(a("body")),this.$lightbox=a("#lightbox"),this.$overlay=a("#lightboxOverlay"),this.$outerContainer=this.$lightbox.find(".lb-outerContainer"),this.$container=this.$lightbox.find(".lb-container"),this.$image=this.$lightbox.find(".lb-image"),this.$nav=this.$lightbox.find(".lb-nav"),this.containerPadding={top:parseInt(this.$container.css("padding-top"),10),right:parseInt(this.$container.css("padding-right"),10),bottom:parseInt(this.$container.css("padding-bottom"),10),left:parseInt(this.$container.css("padding-left"),10)},this.imageBorderWidth={top:parseInt(this.$image.css("border-top-width"),10),right:parseInt(this.$image.css("border-right-width"),10),bottom:parseInt(this.$image.css("border-bottom-width"),10),left:parseInt(this.$image.css("border-left-width"),10)},this.$overlay.hide().on("click",function(){return b.end(),!1}),this.$lightbox.hide().on("click",function(c){"lightbox"===a(c.target).attr("id")&&b.end()}),this.$outerContainer.on("click",function(c){return"lightbox"===a(c.target).attr("id")&&b.end(),!1}),this.$lightbox.find(".lb-prev").on("click",function(){return 0===b.currentImageIndex?b.changeImage(b.album.length-1):b.changeImage(b.currentImageIndex-1),!1}),this.$lightbox.find(".lb-next").on("click",function(){return b.currentImageIndex===b.album.length-1?b.changeImage(0):b.changeImage(b.currentImageIndex+1),!1}),this.$nav.on("mousedown",function(a){3===a.which&&(b.$nav.css("pointer-events","none"),b.$lightbox.one("contextmenu",function(){setTimeout(function(){this.$nav.css("pointer-events","auto")}.bind(b),0)}))}),this.$lightbox.find(".lb-loader, .lb-close").on("click",function(){return b.end(),!1})}},b.prototype.start=function(b){function c(a){d.album.push({alt:a.attr("data-alt"),link:a.attr("href"),title:a.attr("data-title")||a.attr("title")})}var d=this,e=a(window);e.on("resize",a.proxy(this.sizeOverlay,this)),this.sizeOverlay(),this.album=[];var f,g=0,h=b.attr("data-lightbox");if(h){f=a(b.prop("tagName")+'[data-lightbox="'+h+'"]');for(var i=0;ik||g.height>j)&&(g.width/k>g.height/j?(i=k,h=parseInt(g.height/(g.width/i),10),f.width(i),f.height(h)):(h=j,i=parseInt(g.width/(g.height/h),10),f.width(i),f.height(h))),c.sizeContainer(f.width(),f.height())},g.src=this.album[b].link,this.currentImageIndex=b},b.prototype.sizeOverlay=function(){var b=this;setTimeout(function(){b.$overlay.width(a(document).width()).height(a(document).height())},0)},b.prototype.sizeContainer=function(a,b){function c(){d.$lightbox.find(".lb-dataContainer").width(g),d.$lightbox.find(".lb-prevLink").height(h),d.$lightbox.find(".lb-nextLink").height(h),d.$overlay.focus(),d.showImage()}var d=this,e=this.$outerContainer.outerWidth(),f=this.$outerContainer.outerHeight(),g=a+this.containerPadding.left+this.containerPadding.right+this.imageBorderWidth.left+this.imageBorderWidth.right,h=b+this.containerPadding.top+this.containerPadding.bottom+this.imageBorderWidth.top+this.imageBorderWidth.bottom;e!==g||f!==h?this.$outerContainer.animate({width:g,height:h},this.options.resizeDuration,"swing",function(){c()}):c()},b.prototype.showImage=function(){this.$lightbox.find(".lb-loader").stop(!0).hide(),this.$lightbox.find(".lb-image").fadeIn(this.options.imageFadeDuration),this.updateNav(),this.updateDetails(),this.preloadNeighboringImages(),this.enableKeyboardNav()},b.prototype.updateNav=function(){var a=!1;try{document.createEvent("TouchEvent"),a=!!this.options.alwaysShowNavOnTouchDevices}catch(a){}this.$lightbox.find(".lb-nav").show(),this.album.length>1&&(this.options.wrapAround?(a&&this.$lightbox.find(".lb-prev, .lb-next").css("opacity","1"),this.$lightbox.find(".lb-prev, .lb-next").show()):(this.currentImageIndex>0&&(this.$lightbox.find(".lb-prev").show(),a&&this.$lightbox.find(".lb-prev").css("opacity","1")),this.currentImageIndex1&&this.options.showImageNumberLabel){var c=this.imageCountLabel(this.currentImageIndex+1,this.album.length);this.$lightbox.find(".lb-number").text(c).fadeIn("fast")}else this.$lightbox.find(".lb-number").hide();this.$outerContainer.removeClass("animating"),this.$lightbox.find(".lb-dataContainer").fadeIn(this.options.resizeDuration,function(){return a.sizeOverlay()})},b.prototype.preloadNeighboringImages=function(){if(this.album.length>this.currentImageIndex+1){(new Image).src=this.album[this.currentImageIndex+1].link}if(this.currentImageIndex>0){(new Image).src=this.album[this.currentImageIndex-1].link}},b.prototype.enableKeyboardNav=function(){this.$lightbox.on("keyup.keyboard",a.proxy(this.keyboardAction,this)),this.$overlay.on("keyup.keyboard",a.proxy(this.keyboardAction,this))},b.prototype.disableKeyboardNav=function(){this.$lightbox.off(".keyboard"),this.$overlay.off(".keyboard")},b.prototype.keyboardAction=function(a){var b=a.keyCode;27===b?(a.stopPropagation(),this.end()):37===b?0!==this.currentImageIndex?this.changeImage(this.currentImageIndex-1):this.options.wrapAround&&this.album.length>1&&this.changeImage(this.album.length-1):39===b&&(this.currentImageIndex!==this.album.length-1?this.changeImage(this.currentImageIndex+1):this.options.wrapAround&&this.album.length>1&&this.changeImage(0))},b.prototype.end=function(){this.disableKeyboardNav(),a(window).off("resize",this.sizeOverlay),this.$lightbox.fadeOut(this.options.fadeDuration),this.$overlay.fadeOut(this.options.fadeDuration),this.options.disableScrolling&&a("body").removeClass("lb-disable-scrolling")},new b}); +//# sourceMappingURL=lightbox.min.map \ No newline at end of file diff --git a/static/vendor/pdfobject/pdfobject.min.js b/static/vendor/pdfobject/pdfobject.min.js new file mode 100644 index 00000000..cdf5e69b --- /dev/null +++ b/static/vendor/pdfobject/pdfobject.min.js @@ -0,0 +1,9 @@ +/** + * PDFObject v2.2.7 + * https://github.com/pipwerks/PDFObject + * @license + * Copyright (c) 2008-2021 Philip Hutchison + * MIT-style license: http://pipwerks.mit-license.org/ + * UMD module pattern from https://github.com/umdjs/umd/blob/master/templates/returnExports.js + */ +!function(root,factory){"function"==typeof define&&define.amd?define([],factory):"object"==typeof module&&module.exports?module.exports=factory():root.PDFObject=factory()}(this,function(){"use strict";if("undefined"==typeof window||void 0===window.navigator||void 0===window.navigator.userAgent||void 0===window.navigator.mimeTypes)return!1;let nav=window.navigator,ua=window.navigator.userAgent,isIE="ActiveXObject"in window,isModernBrowser=void 0!==window.Promise,supportsPdfMimeType=void 0!==nav.mimeTypes["application/pdf"],isMobileDevice=void 0!==nav.platform&&"MacIntel"===nav.platform&&void 0!==nav.maxTouchPoints&&nav.maxTouchPoints>1||/Mobi|Tablet|Android|iPad|iPhone/.test(ua),isSafariDesktop=!isMobileDevice&&void 0!==nav.vendor&&/Apple/.test(nav.vendor)&&/Safari/.test(ua),isFirefoxWithPDFJS=!!(!isMobileDevice&&/irefox/.test(ua)&&ua.split("rv:").length>1)&&parseInt(ua.split("rv:")[1].split(".")[0],10)>18,createAXO=function(type){var ax;try{ax=new ActiveXObject(type)}catch(e){ax=null}return ax},supportsPDFs=!isMobileDevice&&(isModernBrowser||isFirefoxWithPDFJS||supportsPdfMimeType||isIE&&!(!createAXO("AcroPDF.PDF")&&!createAXO("PDF.PdfCtrl"))),embedError=function(msg,suppressConsole){return suppressConsole||console.log("[PDFObject] "+msg),!1},emptyNodeContents=function(node){for(;node.firstChild;)node.removeChild(node.firstChild)},generatePDFJSMarkup=function(targetNode,url,pdfOpenFragment,PDFJS_URL,id,title,omitInlineStyles){emptyNodeContents(targetNode);let fullURL=PDFJS_URL+"?file="+encodeURIComponent(url)+pdfOpenFragment,div=document.createElement("div"),iframe=document.createElement("iframe");return iframe.src=fullURL,iframe.className="pdfobject",iframe.type="application/pdf",iframe.frameborder="0",iframe.allow="fullscreen",iframe.title=title,id&&(iframe.id=id),omitInlineStyles||(div.style.cssText="position: absolute; top: 0; right: 0; bottom: 0; left: 0;",iframe.style.cssText="border: none; width: 100%; height: 100%;",targetNode.style.position="relative",targetNode.style.overflow="auto"),div.appendChild(iframe),targetNode.appendChild(div),targetNode.classList.add("pdfobject-container"),targetNode.getElementsByTagName("iframe")[0]},embed=function(url,targetSelector,options){let selector=targetSelector||!1,opt=options||{},id="string"==typeof opt.id?opt.id:"",page=opt.page||!1,pdfOpenParams=opt.pdfOpenParams||{},fallbackLink=opt.fallbackLink||!0,width=opt.width||"100%",height=opt.height||"100%",title=opt.title||"Embedded PDF",assumptionMode="boolean"!=typeof opt.assumptionMode||opt.assumptionMode,forcePDFJS="boolean"==typeof opt.forcePDFJS&&opt.forcePDFJS,supportRedirect="boolean"==typeof opt.supportRedirect&&opt.supportRedirect,omitInlineStyles="boolean"==typeof opt.omitInlineStyles&&opt.omitInlineStyles,suppressConsole="boolean"==typeof opt.suppressConsole&&opt.suppressConsole,forceIframe="boolean"==typeof opt.forceIframe&&opt.forceIframe,PDFJS_URL=opt.PDFJS_URL||!1,targetNode=function(targetSelector){let targetNode=document.body;return"string"==typeof targetSelector?targetNode=document.querySelector(targetSelector):void 0!==window.jQuery&&targetSelector instanceof jQuery&&targetSelector.length?targetNode=targetSelector.get(0):void 0!==targetSelector.nodeType&&1===targetSelector.nodeType&&(targetNode=targetSelector),targetNode}(selector),fallbackHTML="",pdfOpenFragment="";if("string"!=typeof url)return embedError("URL is not valid",suppressConsole);if(!targetNode)return embedError("Target element cannot be determined",suppressConsole);if(page&&(pdfOpenParams.page=page),pdfOpenFragment=function(pdfParams){let prop,string="";if(pdfParams){for(prop in pdfParams)pdfParams.hasOwnProperty(prop)&&(string+=encodeURIComponent(prop)+"="+encodeURIComponent(pdfParams[prop])+"&");string&&(string=(string="#"+string).slice(0,string.length-1))}return string}(pdfOpenParams),forcePDFJS&&PDFJS_URL)return generatePDFJSMarkup(targetNode,url,pdfOpenFragment,PDFJS_URL,id,title,omitInlineStyles);if(supportsPDFs||assumptionMode&&!isMobileDevice){return function(embedType,targetNode,targetSelector,url,pdfOpenFragment,width,height,id,title,omitInlineStyles){emptyNodeContents(targetNode);let embed=document.createElement(embedType);if(embed.src=url+pdfOpenFragment,embed.className="pdfobject",embed.type="application/pdf",embed.title=title,id&&(embed.id=id),"iframe"===embedType&&(embed.allow="fullscreen"),!omitInlineStyles){let style="embed"===embedType?"overflow: auto;":"border: none;";targetSelector&&targetSelector!==document.body?style+="width: "+width+"; height: "+height+";":style+="position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%;",embed.style.cssText=style}return targetNode.classList.add("pdfobject-container"),targetNode.appendChild(embed),targetNode.getElementsByTagName(embedType)[0]}(forceIframe||supportRedirect||isSafariDesktop?"iframe":"embed",targetNode,targetSelector,url,pdfOpenFragment,width,height,id,title,omitInlineStyles)}return PDFJS_URL?generatePDFJSMarkup(targetNode,url,pdfOpenFragment,PDFJS_URL,id,title,omitInlineStyles):(fallbackLink&&(fallbackHTML="string"==typeof fallbackLink?fallbackLink:"

This browser does not support inline PDFs. Please download the PDF to view it: Download PDF

",targetNode.innerHTML=fallbackHTML.replace(/\[url\]/g,url)),embedError("This browser does not support embedded PDFs",suppressConsole))};return{embed:function(a,b,c){return embed(a,b,c)},pdfobjectversion:"2.2.7",supportsPDFs:supportsPDFs}}); \ No newline at end of file diff --git a/templates/webclient/editfile.html b/templates/webclient/editfile.html index 4febea0e..eed629fb 100644 --- a/templates/webclient/editfile.html +++ b/templates/webclient/editfile.html @@ -36,7 +36,9 @@ Edit file "{{.Path}}" Back + {{if not .ReadOnly}} Save + {{end}} @@ -107,6 +109,9 @@ lineNumbers: true, styleActiveLine: true, extraKeys: {"Alt-F": "findPersistent"}, + {{if .ReadOnly}} + readOnly: true, + {{end}} autofocus: true }); var filename = "{{.Path}}"; @@ -126,6 +131,7 @@ }); } + {{if not .ReadOnly}} function saveFile() { $('#idSave').addClass("disabled"); cm = document.querySelector('.CodeMirror').CodeMirror; @@ -167,5 +173,6 @@ } }); } + {{end}} {{end}} \ No newline at end of file diff --git a/templates/webclient/files.html b/templates/webclient/files.html index be307cb1..3d6831b1 100644 --- a/templates/webclient/files.html +++ b/templates/webclient/files.html @@ -8,6 +8,7 @@ +