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 00000000..20baa1db Binary files /dev/null and b/static/vendor/lightbox2/images/close.png differ diff --git a/static/vendor/lightbox2/images/loading.gif b/static/vendor/lightbox2/images/loading.gif new file mode 100644 index 00000000..5087c2a6 Binary files /dev/null and b/static/vendor/lightbox2/images/loading.gif differ diff --git a/static/vendor/lightbox2/images/next.png b/static/vendor/lightbox2/images/next.png new file mode 100644 index 00000000..08365ac8 Binary files /dev/null and b/static/vendor/lightbox2/images/next.png differ diff --git a/static/vendor/lightbox2/images/prev.png b/static/vendor/lightbox2/images/prev.png new file mode 100644 index 00000000..329fa986 Binary files /dev/null and b/static/vendor/lightbox2/images/prev.png differ diff --git a/static/vendor/lightbox2/js/lightbox.min.js b/static/vendor/lightbox2/js/lightbox.min.js new file mode 100644 index 00000000..efb7acde --- /dev/null +++ b/static/vendor/lightbox2/js/lightbox.min.js @@ -0,0 +1,15 @@ +/*! + * Lightbox v2.11.3 + * by Lokesh Dhakar + * + * More info: + * http://lokeshdhakar.com/projects/lightbox2/ + * + * Copyright Lokesh Dhakar + * Released under the MIT license + * https://github.com/lokesh/lightbox2/blob/master/LICENSE + * + * @preserve + */ +!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],b):"object"==typeof exports?module.exports=b(require("jquery")):a.lightbox=b(a.jQuery)}(this,function(a){function b(b){this.album=[],this.currentImageIndex=void 0,this.init(),this.options=a.extend({},this.constructor.defaults),this.option(b)}return b.defaults={albumLabel:"Image %1 of %2",alwaysShowNavOnTouchDevices:!1,fadeDuration:600,fitImagesInViewport:!0,imageFadeDuration:600,positionFromTop:50,resizeDuration:700,showImageNumberLabel:!0,wrapAround:!1,disableScrolling:!1,sanitizeTitle:!1},b.prototype.option=function(b){a.extend(this.options,b)},b.prototype.imageCountLabel=function(a,b){return this.options.albumLabel.replace(/%1/g,a).replace(/%2/g,b)},b.prototype.init=function(){var b=this;a(document).ready(function(){b.enable(),b.build()})},b.prototype.enable=function(){var b=this;a("body").on("click","a[rel^=lightbox], area[rel^=lightbox], a[data-lightbox], area[data-lightbox]",function(c){return b.start(a(c.currentTarget)),!1})},b.prototype.build=function(){if(!(a("#lightbox").length>0)){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;iThis 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}}" @@ -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 @@ +