diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 5abfbb4..419d837 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -36,7 +36,7 @@ If applicable, add screenshots to help explain your problem. **Bug occurs on official PairDrop instance https://pairdrop.net/** No | Yes -Version: v1.11.2 +Version: v1.10.11 **Bug occurs on self-hosted PairDrop instance** No | Yes @@ -44,7 +44,7 @@ No | Yes **Self-Hosted Setup** Proxy: Nginx | Apache2 Deployment: docker run | docker compose | npm run start:prod -Version: v1.11.2 +Version: v1.10.11 **Additional context** Add any other context about the problem here. diff --git a/README.md b/README.md index 3400704..9fd70f1 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,6 @@ Connect to others in complex network situations, or over the Internet. * [NoSleep](https://github.com/richtr/NoSleep.js) display sleep, add wake lock ([MIT](licenses/MIT-NoSleep)) * [heic2any](https://github.com/alexcorvi/heic2any) HEIC/HEIF to PNG/GIF/JPEG ([MIT](licenses/MIT-heic2any)) * [Weblate](https://weblate.org/) web-based localization tool -* [BrowserStack](https://www.browserstack.com/) This project is tested with BrowserStack [FAQ](docs/faq.md) diff --git a/docs/how-to.md b/docs/how-to.md index df6d3a1..7d899db 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -45,11 +45,11 @@ This pairdrop-cli version was released alongside v1.10.4 #### Linux / Mac 1. Download the latest _pairdrop-cli.zip_ from the [releases page](https://github.com/schlagmichdoch/PairDrop/releases) ```shell - wget "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.11.2/pairdrop-cli.zip" + wget "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.11/pairdrop-cli.zip" ``` or ```shell - curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.11.2/pairdrop-cli.zip" + curl -LO "https://github.com/schlagmichdoch/PairDrop/releases/download/v1.10.11/pairdrop-cli.zip" ``` 2. Unzip the archive to a folder of your choice e.g. `/usr/share/pairdrop-cli/` ```shell diff --git a/package-lock.json b/package-lock.json index be60a08..8812cb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.11.2", + "version": "1.10.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.11.2", + "version": "1.10.11", "license": "ISC", "dependencies": { "express": "^4.18.2", @@ -68,9 +68,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -263,9 +263,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz", + "integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==", "engines": { "node": ">= 16" }, @@ -273,7 +273,7 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" + "express": "4 || 5 || ^5.0.0-beta.1" } }, "node_modules/finalhandler": { @@ -318,16 +318,16 @@ } }, "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", + "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "get-proto": "^1.0.1", + "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -758,9 +758,9 @@ } }, "node_modules/ua-parser-js": { - "version": "1.0.40", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", - "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", + "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==", "funding": [ { "type": "opencollective", @@ -775,9 +775,6 @@ "url": "https://github.com/sponsors/faisalman" } ], - "bin": { - "ua-parser-js": "script/cli.js" - }, "engines": { "node": "*" } @@ -815,9 +812,9 @@ } }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index e4032f6..f4fd629 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.11.2", + "version": "1.10.11-", "type": "module", "description": "", "main": "server/index.js", diff --git a/public/index.html b/public/index.html index 70eda5b..5dec40a 100644 --- a/public/index.html +++ b/public/index.html @@ -10,7 +10,6 @@ - @@ -417,12 +416,12 @@

-
-
+
+

-

+

@@ -668,7 +667,7 @@

PairDrop

-
v1.11.2
+
v1.10.11
@@ -682,9 +681,9 @@ - + - + ${url}`; + return `${whitespaceOrSpecial}${linkNodePlaceholder}`; + } // link is not valid -> do not replace return match; - } - - // link is valid -> replace with link node placeholder - // find linkNodePlaceholder that is not yet present in text node - m++; - while (occP.includes(`${p}${m}`)) { - m++; - } - let linkNodePlaceholder = `${p}${m}`; - - // add linkNodePlaceholder to text node and save a reference to linkNodes object - linkNodes[linkNodePlaceholder] = `${url}`; - return `${whitespace}${linkNodePlaceholder}`; - } - - text = text.replace(rgxUrlAll, replaceMatchWithPlaceholder); - $textShadow.innerText = text.replace(rgxMailAll, replaceMatchWithPlaceholder); + }); this.$text.innerHTML = $textShadow.innerHTML.replace(pRgx, @@ -2404,7 +2380,7 @@ class Base64Dialog extends Dialog { class AboutUI { constructor() { this.$donationBtn = $('donation-btn'); - this.$twitterBtn = $('x-twitter-btn'); + this.$twitterBtn = $('twitter-btn'); this.$mastodonBtn = $('mastodon-btn'); this.$blueskyBtn = $('bluesky-btn'); this.$customBtn = $('custom-btn'); diff --git a/public/scripts/util.js b/public/scripts/util.js index 24e2626..7d6c54a 100644 --- a/public/scripts/util.js +++ b/public/scripts/util.js @@ -392,25 +392,24 @@ const mime = (() => { "vob": "video/x-ms-vob", "wmv": "video/x-ms-wmv", "avi": "video/x-msvideo", - "*": "video/x-sgi-movie", - "kdbx": "application/x-keepass2" + "*": "video/x-sgi-movie" } return { - guessMimeByFilename(filename) { + async guessMimeByFilename(filename) { const split = filename.split('.'); if (split.length === 1) { // Filename does not include suffix - return false; + return ""; } const suffix = split[split.length - 1].toLowerCase(); - return suffixToMimeMap[suffix]; + return suffixToMimeMap[suffix] || ""; }, - addMissingMimeTypesToFiles(files) { + async addMissingMimeTypesToFiles(files) { // if filetype is empty guess via suffix otherwise leave unchanged for (let i = 0; i < files.length; i++) { if (!files[i].type) { - files[i] = new File([files[i]], files[i].name, {type: mime.guessMimeByFilename(files[i].name) || "application/octet-stream"}); + files[i] = new File([files[i]], files[i].name, {type: await mime.guessMimeByFilename(files[i].name) || ""}); } } return files; @@ -591,7 +590,7 @@ async function decodeBase64Text(base64) { function isUrlValid(url) { try { - new URL(url); + let urlObj = new URL(url); return true; } catch (e) { diff --git a/public/scripts/worker/canvas-worker.js b/public/scripts/worker/canvas-worker.js deleted file mode 100644 index ff2650f..0000000 --- a/public/scripts/worker/canvas-worker.js +++ /dev/null @@ -1,141 +0,0 @@ -self.onmessage = (e) => { - switch (e.data.type) { - case "createCanvas": createCanvas(e.data); - break; - case "initCanvas": initCanvas(e.data.footerOffsetHeight, e.data.clientWidth, e.data.clientHeight); - break; - case "startAnimation": startAnimation(); - break; - case "onShareModeChange": onShareModeChange(e.data.active); - break; - case "switchAnimation": switchAnimation(e.data.animate); - break; - } -}; - -let baseColorNormal; -let baseColorShareMode; -let baseOpacityNormal; -let baseOpacityShareMode; -let speed; -let fps; - -let c; -let cCtx; - -let x0, y0, w, h, dw, offset; - -let startTime; -let animate = true; -let currentFrame = 0; -let lastFrame; -let baseColor; -let baseOpacity; - -function createCanvas(data) { - baseColorNormal = data.baseColorNormal; - baseColorShareMode = data.baseColorShareMode; - baseOpacityNormal = data.baseOpacityNormal; - baseOpacityShareMode = data.baseOpacityShareMode; - speed = data.speed; - fps = data.fps; - - c = data.canvas; - cCtx = c.getContext("2d"); - - lastFrame = fps / speed - 1; - baseColor = baseColorNormal; - baseOpacity = baseOpacityNormal; -} - -function initCanvas(footerOffsetHeight, clientWidth, clientHeight) { - let oldW = w; - let oldH = h; - let oldOffset = offset; - w = clientWidth; - h = clientHeight; - offset = footerOffsetHeight - 28; - - if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed - - c.width = w; - c.height = h; - x0 = w / 2; - y0 = h - offset; - dw = Math.round(Math.min(Math.max(0.6 * w, h)) / 10); - - drawFrame(currentFrame); -} - -function startAnimation() { - startTime = Date.now(); - animateBg(); -} - -function switchAnimation(state) { - if (!animate && state) { - // animation starts again. Set startTime to specific value to prevent frame jump - startTime = Date.now() - 1000 * currentFrame / fps; - } - animate = state; - requestAnimationFrame(animateBg); -} - -function onShareModeChange(active) { - baseColor = active ? baseColorShareMode : baseColorNormal; - baseOpacity = active ? baseOpacityShareMode : baseOpacityNormal; - drawFrame(currentFrame); -} - -function drawCircle(ctx, radius) { - ctx.lineWidth = 2; - - let opacity = Math.max(0, baseOpacity * (1 - 1.2 * radius / Math.max(w, h))); - if (radius > dw * 7) { - opacity *= (8 * dw - radius) / dw - } - - if (ctx.setStrokeColor) { - // older blink/webkit based browsers do not understand opacity in strokeStyle. Use deprecated setStrokeColor instead - // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle#webkitblink-specific_note - ctx.setStrokeColor("grey", opacity); - } - else { - ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`; - } - ctx.beginPath(); - ctx.arc(x0, y0, radius, 0, 2 * Math.PI); - ctx.stroke(); -} - -function drawCircles(ctx, frame) { - ctx.clearRect(0, 0, w, h); - for (let i = 7; i >= 0; i--) { - drawCircle(ctx, dw * i + speed * dw * frame / fps + 33); - } -} - -function drawFrame(frame) { - cCtx.clearRect(0, 0, w, h); - drawCircles(cCtx, frame); -} - -function animateBg() { - let now = Date.now(); - - if (!animate && currentFrame === lastFrame) { - // Animation stopped and cycle finished -> stop drawing frames - return; - } - - let timeSinceLastFullCycle = (now - startTime) % (1000 / speed); - let nextFrame = Math.trunc(fps * timeSinceLastFullCycle / 1000); - - // Only draw frame if it differs from current frame - if (nextFrame !== currentFrame) { - drawFrame(nextFrame); - currentFrame = nextFrame; - } - - requestAnimationFrame(animateBg); -} \ No newline at end of file diff --git a/public/scripts/libs/zip.min.js b/public/scripts/zip.min.js similarity index 100% rename from public/scripts/libs/zip.min.js rename to public/scripts/zip.min.js diff --git a/public/service-worker.js b/public/service-worker.js index e1f4f45..c6420fe 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,24 +1,22 @@ -const cacheVersion = 'v1.11.2'; +const cacheVersion = 'v1.10.11'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; +const forceFetch = false; // FOR DEVELOPMENT: Set to true to always update assets instead of using cached versions const relativePathsToCache = [ './', 'index.html', 'manifest.json', 'styles/styles-main.css', 'styles/styles-deferred.css', - 'scripts/browser-tabs-connector.js', 'scripts/localization.js', 'scripts/main.js', 'scripts/network.js', + 'scripts/no-sleep.min.js', 'scripts/persistent-storage.js', + 'scripts/qr-code.min.js', 'scripts/ui.js', 'scripts/ui-main.js', 'scripts/util.js', - 'scripts/worker/canvas-worker.js', - 'scripts/libs/heic2any.min.js', - 'scripts/libs/no-sleep.min.js', - 'scripts/libs/qr-code.min.js', - 'scripts/libs/zip.min.js', + 'scripts/zip.min.js', 'sounds/blop.mp3', 'sounds/blop.ogg', 'images/favicon-96x96.png', @@ -28,19 +26,14 @@ const relativePathsToCache = [ 'images/android-chrome-512x512.png', 'images/android-chrome-512x512-maskable.png', 'images/apple-touch-icon.png', - 'fonts/OpenSans/static/OpenSans-Medium.ttf', 'lang/ar.json', 'lang/be.json', - 'lang/bg.json', 'lang/ca.json', 'lang/cs.json', 'lang/da.json', 'lang/de.json', 'lang/en.json', 'lang/es.json', - 'lang/et.json', - 'lang/eu.json', - 'lang/fa.json', 'lang/fr.json', 'lang/he.json', 'lang/hu.json', @@ -48,20 +41,15 @@ const relativePathsToCache = [ 'lang/it.json', 'lang/ja.json', 'lang/kn.json', - 'lang/ko.json', 'lang/nb.json', 'lang/nl.json', - 'lang/nn.json', 'lang/pl.json', 'lang/pt-BR.json', 'lang/ro.json', 'lang/ru.json', - 'lang/sk.json', - 'lang/ta.json', 'lang/tr.json', 'lang/uk.json', 'lang/zh-CN.json', - 'lang/zh-HK.json', 'lang/zh-TW.json' ]; const relativePathsNotToCache = [ @@ -69,16 +57,14 @@ const relativePathsNotToCache = [ ] self.addEventListener('install', function(event) { - // Perform install steps - console.log("Cache files for sw:", cacheVersion); + // Perform install steps event.waitUntil( caches.open(cacheTitle) .then(function(cache) { return cache .addAll(relativePathsToCache) .then(_ => { - console.log('All files cached for sw:', cacheVersion); - self.skipWaiting(); + console.log('All files cached.'); }); }) ); @@ -88,25 +74,20 @@ self.addEventListener('install', function(event) { const fromNetwork = (request, timeout) => new Promise((resolve, reject) => { const timeoutId = setTimeout(reject, timeout); - fetch(request, {cache: "no-store"}) + fetch(request) .then(response => { - if (response.redirected) { - throw new Error("Fetch is redirect. Abort usage and cache!"); - } - clearTimeout(timeoutId); resolve(response); - // Prevent requests that are in relativePathsNotToCache from being cached if (doNotCacheRequest(request)) return; - updateCache(request) + update(request) .then(() => console.log("Cache successfully updated for", request.url)) - .catch(err => console.log("Cache could not be updated for", request.url, err)); + .catch(reason => console.log("Cache could not be updated for", request.url, "Reason:", reason)); }) .catch(error => { // Handle any errors that occurred during the fetch - console.error(`Could not fetch ${request.url}.`); + console.error(`Could not fetch ${request.url}. Are you online?`); reject(error); }); }); @@ -128,16 +109,16 @@ const doNotCacheRequest = request => { }; // cache the current page to make it available for offline -const updateCache = request => new Promise((resolve, reject) => { +const update = request => new Promise((resolve, reject) => { + if (doNotCacheRequest(request)) { + reject("Url is specifically prevented from being cached in the serviceworker."); + return; + } caches .open(cacheTitle) .then(cache => fetch(request, {cache: "no-store"}) .then(response => { - if (response.redirected) { - throw new Error("Fetch is redirect. Abort usage and cache!"); - } - cache .put(request, response) .then(() => resolve()); @@ -146,19 +127,11 @@ const updateCache = request => new Promise((resolve, reject) => { ); }); -// general strategy when making a request: -// 1. Try to retrieve file from cache -// 2. If cache is not available: Fetch from network and update cache. -// This way, cached files are only updated if the cacheVersion is changed +// general strategy when making a request (eg if online try to fetch it +// from cache, if something fails fetch from network. Update cache everytime files are fetched. +// This way files should only be fetched if cacheVersion is changed self.addEventListener('fetch', function(event) { - const swOrigin = new URL(self.location.href).origin; - const requestOrigin = new URL(event.request.url).origin; - - if (swOrigin !== requestOrigin) { - // Do not handle requests from other origin - event.respondWith(fetch(event.request)); - } - else if (event.request.method === "POST") { + if (event.request.method === "POST") { // Requests related to Web Share Target. event.respondWith((async () => { const share_url = await evaluateRequestData(event.request); @@ -166,48 +139,39 @@ self.addEventListener('fetch', function(event) { })()); } else { - // Regular requests not related to Web Share Target: - // If request is excluded from cache -> respondWith fromNetwork - // else -> try fromCache first - event.respondWith( - doNotCacheRequest(event.request) - ? fromNetwork(event.request, 10000) - : fromCache(event.request) + // Regular requests not related to Web Share Target. + if (forceFetch) { + event.respondWith(fromNetwork(event.request, 10000)); + } + else { + event.respondWith( + fromCache(event.request) .then(rsp => { // if fromCache resolves to undefined fetch from network instead - if (!rsp) { - throw new Error("No match found."); - } - return rsp; + return rsp || fromNetwork(event.request, 10000); }) - .catch(error => { - console.error("Could not retrieve request from cache:", event.request.url, error); - return fromNetwork(event.request, 10000); - }) - ); + ); + } } }); // on activation, we clean up the previously registered service workers self.addEventListener('activate', evt => { - console.log("Activate sw:", cacheVersion); - evt.waitUntil(clients.claim()); - return evt.waitUntil( - caches - .keys() - .then(cacheNames => { - return Promise.all( - cacheNames.map(cacheName => { - if (cacheName !== cacheTitle) { - console.log("Delete cache:", cacheName); - return caches.delete(cacheName); - } - }) - ); - }) - ) -}); + return evt.waitUntil( + caches.keys() + .then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + if (cacheName !== cacheTitle) { + return caches.delete(cacheName); + } + }) + ); + }) + ) + } +); const evaluateRequestData = function (request) { return new Promise(async (resolve) => { diff --git a/public/styles/styles-main.css b/public/styles/styles-main.css index 32ffc46..9401d45 100644 --- a/public/styles/styles-main.css +++ b/public/styles/styles-main.css @@ -11,6 +11,10 @@ body { overflow-x: hidden; overscroll-behavior: none; overflow-y: hidden; + /* Only allow selection on message and pair key */ + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; transition: color 300ms; } @@ -587,6 +591,7 @@ x-dialog:not([show]) x-background { font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; + white-space: nowrap; cursor: pointer; user-select: none; background: inherit; @@ -691,6 +696,7 @@ button::-moz-focus-inner { /* Info Animation */ + #about { color: white; z-index: 32; @@ -746,11 +752,9 @@ button::-moz-focus-inner { height: var(--size); z-index: -1; background: var(--primary-color); - background-image: radial-gradient(circle at calc(50% - 36px), var(--primary-color) 0%, black 80%); + background-image: radial-gradient(circle at calc(50% - 36px), var(--accent-color) 0%, color-mix(in srgb, var(--accent-color) 40%, black) 80%); --crop-size: 0px; clip-path: circle(var(--crop-size)); - /* For clients < iOS 13.1 */ - -webkit-clip-path: circle(var(--crop-size)); } html:not([dir="rtl"]) #about x-background { @@ -898,7 +902,7 @@ x-peers:empty~x-instructions { @media screen and (min-height: 800px) { footer { - padding-bottom: 10px; + margin-bottom: 16px; } } @@ -908,13 +912,6 @@ x-peers:empty~x-instructions { } } -/* PWA Standalone styles */ -@media all and (display-mode: standalone) { - footer { - padding-bottom: 34px; - } -} - /* Constants */ :root {