diff --git a/apple/Sources/SafariExtension/Resources/manifest.json b/apple/Sources/SafariExtension/Resources/manifest.json index a5c39db66..e59fa2d65 100644 --- a/apple/Sources/SafariExtension/Resources/manifest.json +++ b/apple/Sources/SafariExtension/Resources/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "Omnivore", "short_name": "Omnivore", - "version": "0.1.22", + "version": "0.1.24", "description": "Save articles to your Omnivore library", "author": "Omnivore Media, Inc", "default_locale": "en", @@ -11,7 +11,7 @@ "url": "https://omnivore.app/" }, "homepage_url": "https://omnivore.app/", - "content_security_policy": "default-src 'none'; child-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; worker-src 'none'; connect-src https://storage.googleapis.com/ https://omnivore.app/api/ blob:; frame-src 'none'; font-src 'none'; img-src data:; script-src 'self'; script-src-elem 'self'; script-src-attr 'none'; style-src 'self'; style-src-elem 'self'; style-src-attr 'none'; base-uri 'none'; form-action 'none'; block-all-mixed-content; upgrade-insecure-requests; report-uri https://api.jeurissen.co/reports/csp/webext/omnivore/", + "content_security_policy": "default-src 'none'; child-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; worker-src 'none'; connect-src https://storage.googleapis.com/ https://api-prod.omnivore.app/api/ blob:; frame-src 'none'; font-src 'none'; img-src data:; script-src 'self'; script-src-elem 'self'; script-src-attr 'none'; style-src 'self'; style-src-elem 'self'; style-src-attr 'none'; base-uri 'none'; form-action 'none'; block-all-mixed-content; upgrade-insecure-requests; report-uri https://api.jeurissen.co/reports/csp/webext/omnivore/", "icons": { "16": "/images/extension/icon-16.png", "24": "/images/extension/icon-24.png", @@ -80,6 +80,14 @@ }, "default_title": "Omnivore Save Article" }, + "commands": { + "_execute_browser_action": { + "suggested_key": { + "default": "Alt + O" + }, + "description": "Save the current tab to Omnivore" + } + }, "web_accessible_resources": [ "views/cta-popup.html" ] diff --git a/apple/Sources/SafariExtension/Resources/scripts/background.js b/apple/Sources/SafariExtension/Resources/scripts/background.js index 5dcad7d2f..62ba715e6 100644 --- a/apple/Sources/SafariExtension/Resources/scripts/background.js +++ b/apple/Sources/SafariExtension/Resources/scripts/background.js @@ -1 +1 @@ -(()=>{"use strict";let e;const t="https://omnivore.app",s="https://omnivore.app/api/";function o(e){return new Promise((t=>{browserApi.storage.local.get(e,(s=>{const o=s&&s[e]||null;t(o)}))}))}function n(e){return new Promise((t=>{browserApi.storage.local.set(e,t)}))}function r(e){return new Promise((t=>{browserApi.storage.local.remove(e,t)}))}function a(t){t.setRequestHeader("Content-Type","application/json"),e&&t.setRequestHeader("Authorization",e)}function i(){o("postInstallClickComplete").then((e=>{e&&r("postInstallClickComplete")}))}function l(e){browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{type:"loading",text:"Saving..."}});const o=new XMLHttpRequest,n={BAD_DATA:"Unable to save page",NOT_ALLOWED_TO_PARSE:"Not allowed to parse this article",UNAUTHORIZED:"Please login to Omnivore to authorize this action",UNABLE_TO_FETCH:"Unable to fetch page",PAYLOAD_TOO_LARGE:"This article is too large"};o.onreadystatechange=function(){if(4===o.readyState)if(200===o.status){const{data:s}=JSON.parse(o.response);if("createArticle"in s)if("errorCodes"in s.createArticle){const o={text:n[s.createArticle.errorCodes[0]]||"Unable to save page",type:"error"};"UNAUTHORIZED"===s.createArticle.errorCodes[0]&&(o.errorCode=401,o.url=t,i()),browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:o})}else{const o=s.createArticle.createdArticle,n=s.createArticle.user,r=t+(o.hasContent?`/${n.profile.username}/`+o.slug:"/home");browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Saved to Omnivore",link:r,linkText:"View",type:"success"}})}else if("errorCodes"in s.createArticleSavingRequest)browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:n[s.createArticleSavingRequest.errorCodes[0]]||"Unable to save page",type:"error"}});else{const o=s.createArticleSavingRequest.articleSavingRequest,n=t+"/article/sr/"+o.id;browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Saved to Omnivore",link:n,linkText:"View",type:"success"}})}}else 400===o.status&&browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Unable to save page",type:"error"}})},browserApi.tabs.sendMessage(e.id,{action:ACTIONS.GetContent},(async n=>{if(!n||"object"!=typeof n)return;const{type:r,pageInfo:l,doc:c,uploadContentObjUrl:p}=n;let d=null;switch(r){case"pdf":if(d=await function(e,o,n,r){return new Promise((l=>{const c=new XMLHttpRequest;c.onreadystatechange=async function(){if(4===c.readyState){if(200===c.status){const{data:s}=JSON.parse(c.response);if("errorCodes"in s.uploadFileRequest&&"UNAUTHORIZED"===s.uploadFileRequest.errorCodes[0]&&(i(),browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Unable to save page",type:"error",errorCode:401,url:t}})),s.uploadFileRequest&&s.uploadFileRequest.id&&!("errorCodes"in s.uploadFileRequest)){const e=await function({id:e,uploadSignedUrl:t},s,o){return fetch(o).then((e=>e.blob())).then((o=>new Promise((n=>{const r=new XMLHttpRequest;r.open("PUT",t,!0),r.setRequestHeader("Content-Type",s),r.onerror=()=>{n(null)},r.onload=()=>{n({id:e})},r.send(o)})))).catch((e=>{console.error("error uploading file",e)}))}(s.uploadFileRequest,n,r);return URL.revokeObjectURL(r),l(e)}browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Unable to save page",type:"error"}})}else 400===c.status&&browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Unable to save page",type:"error"}});l(!1)}};const p=JSON.stringify({query:"mutation UploadFileRequest($input: UploadFileRequestInput!) {\n uploadFileRequest(input:$input) {\n ... on UploadFileRequestError {\n errorCodes\n }\n ... on UploadFileRequestSuccess {\n id\n uploadSignedUrl\n }\n }\n }",variables:{input:{url:o,contentType:n}}});c.open("POST",s+"graphql",!0),a(c),c.send(p)}))}(e,encodeURI(e.url),l.contentType,p),!d||!d.id)return void browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Unable to save page",type:"error"}})}const u=c&&c.length||d,g=u?CREATE_ARTICLE_QUERY:CREATE_ARTICLE_SAVING_REQUEST_QUERY,b={url:encodeURI(e.url)};u&&(b.preparedDocument={document:c,pageInfo:l},b.uploadFileId=d&&d.id||null);const A=JSON.stringify({query:g,variables:{input:b}});o.open("POST",s+"graphql",!0),a(o),o.send(A)}))}async function c(e){const t=await o(e+"_saveInProgress");if(!t)return;clearInterval(t);const s=await o(e+"_saveInProgressTimeoutId_"+t);s&&clearTimeout(s)}function p(e){function t(t,s){browserApi.tabs.get(e,(e=>{"complete"!==e.status?(browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{type:"loading",text:"Page loading..."}}),s&&"function"==typeof s&&s()):(t&&"function"==typeof t&&t(),l(e))}))}c(e),t(null,(()=>{!function(e,t,s,o=1e3,n=10500){const r=setInterval(e,o),a=setTimeout((()=>{clearInterval(r),t()}),n);s&&"function"==typeof s&&s(r,a)}((()=>{t((()=>{c(e)}))}),(()=>{c(e),browserApi.tabs.get(e,(e=>{l(e)}))}),((t,s)=>{const o={};o[e+"_saveInProgress"]=t,o[e+"_saveInProgressTimeoutId_"+t]=s,n(o)}))}))}function d(r){return o("postInstallClickComplete").then((async o=>{if(o)return!0;if("undefined"!=typeof browser&&browser.runtime&&browser.runtime.sendNativeMessage){const t=await browser.runtime.sendNativeMessage("omnivore",{message:ACTIONS.GetAuthToken});t.authToken&&(e=t.authToken)}return new Promise((e=>{const o=new XMLHttpRequest;o.onreadystatechange=function(){if(4===o.readyState&&200===o.status){const{data:s}=JSON.parse(o.response);s.me?(n({postInstallClickComplete:!0}),e(!0)):(browserApi.tabs.sendMessage(r,{action:ACTIONS.ShowMessage,payload:{type:"loading",text:"Loading..."}}),browserApi.tabs.sendMessage(r,{action:ACTIONS.ShowMessage,payload:{text:"",type:"error",errorCode:401,url:t}}),e(null))}};const i=JSON.stringify({query:"{me{id}}"});o.open("POST",s+"graphql",!0),a(o),o.send(i)}))}))}function u(e,t){let s="/images/toolbar/icon";if(ENV_IS_FIREFOX?s+="_firefox":ENV_IS_EDGE&&(s+="_edge"),e||(s+="_inactive"),("boolean"==typeof t?t:window.matchMedia("(prefers-color-scheme: dark)").matches)&&(s+="_dark"),ENV_IS_FIREFOX)return s+".svg";const o=["16","24","32","48"];ENV_IS_EDGE||o.push("19","38");const n={};for(let e=0;e{!function t(){browserApi.tabs.get(e,(function(e){browserApi.runtime.lastError&&setTimeout(t,150),b(e)}))}()})),browserApi.tabs.onUpdated.addListener(((e,t,s)=>{t.status&&s&&s.active&&b(s)})),browserApi.tabs.onRemoved.addListener((e=>{!function(e){new Promise((e=>{browserApi.storage.local.get(null,(t=>{e(t||{})}))})).then((function(t){const s=[],o=Object.keys(t),n=e+"_saveInProgress";for(let e=0;e{browserApi.tabs.query({active:!0,currentWindow:!0},(function(t){e(t[0]||null)}))})).then((e=>{browserApi.tabs.sendMessage(e.id,{action:ACTIONS.Ping},(async function(t){if(t&&t.pong)await d(e.id)&&p(e.id);else{const t=browserApi.runtime.getManifest().content_scripts,s=[...t[0].js,...t[1].js];!function(t,s,o){function n(e,t,s){return function(){browserScriptingApi.executeScript(e,t,s)}}let r=async function(){await d(e.id)&&p(e.id)};for(let e=s.length-1;e>=0;--e)r=n(t,{file:s[e]},r);null!==r&&r()}(e.id,s)}}))}))})),browserApi.runtime.onMessage.addListener(((e,t,s)=>{if(e.forwardToTab)return delete e.forwardToTab,void browserApi.tabs.sendRequest(t.tab.id,e);e.action===ACTIONS.RefreshDarkMode&&g(t.tab.id,e.payload.value)})),browserActionApi.setIcon({path:u(!0)})})(); \ No newline at end of file +(()=>{"use strict";var e,t=new Uint8Array(16);function n(){if(!e&&!(e="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return e(t)}const o=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i,s=function(e){return"string"==typeof e&&o.test(e)};for(var r=[],a=0;a<256;++a)r.push((a+256).toString(16).substr(1));const i=function(e,t,o){var a=(e=e||{}).random||(e.rng||n)();if(a[6]=15&a[6]|64,a[8]=63&a[8]|128,t){o=o||0;for(var i=0;i<16;++i)t[o+i]=a[i];return t}return function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=(r[e[t+0]]+r[e[t+1]]+r[e[t+2]]+r[e[t+3]]+"-"+r[e[t+4]]+r[e[t+5]]+"-"+r[e[t+6]]+r[e[t+7]]+"-"+r[e[t+8]]+r[e[t+9]]+"-"+r[e[t+10]]+r[e[t+11]]+r[e[t+12]]+r[e[t+13]]+r[e[t+14]]+r[e[t+15]]).toLowerCase();if(!s(n))throw TypeError("Stringified UUID is invalid");return n}(a)};let u;const c="https://omnivore.app",d="https://api-prod.omnivore.app/api/";function l(e){return new Promise((t=>{browserApi.storage.local.get(e,(n=>{const o=n&&n[e]||null;t(o)}))}))}function p(e){return new Promise((t=>{browserApi.storage.local.set(e,t)}))}function f(e){return new Promise((t=>{browserApi.storage.local.remove(e,t)}))}function g(e){e.setRequestHeader("Content-Type","application/json"),u&&e.setRequestHeader("Authorization",u)}function b(){l("postInstallClickComplete").then((e=>{e&&f("postInstallClickComplete")}))}async function w(e,t,n,o){return browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{type:"loading",text:"Saving..."}}),new Promise(((s,r)=>{const a=new XMLHttpRequest;a.open("POST",d+"graphql",!0),g(a),a.onerror=e=>{r(e)},a.onload=()=>{try{const t=function(e,t,n){if(4===n.readyState){if(200===n.status){const{data:o}=JSON.parse(n.response),s=o[t];if(!s)return;if("errorCodes"in s){const t={text:descriptions[o.createArticle.errorCodes[0]]||"Unable to save page",type:"error"};return"UNAUTHORIZED"===s.errorCodes[0]&&(t.errorCode=401,t.url=c,b()),void browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:t})}const r=s.url;return browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Saved to Omnivore",link:r??c+"/home",linkText:"Read Now",type:"success"}}),s}if(400===n.status)return void browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Unable to save page",type:"error"}})}}(e,n,a);if(!t)return r();s(t)}catch(e){r(e)}};const i=JSON.stringify({query:t,variables:{input:o}});a.send(i)})).catch((t=>{console.log("error saving page",t),browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Unable to save page",type:"error"}})}))}function y(e){browserApi.tabs.sendMessage(e.id,{action:ACTIONS.GetContent},(async t=>{if(!t||"object"!=typeof t)return;const n=i(),{type:o,pageInfo:s,doc:r,uploadContentObjUrl:a}=t;switch(o){case"html":await w(e,SAVE_PAGE_QUERY,"savePage",{source:"extension",clientRequestId:n,originalContent:r,title:s.title,url:encodeURI(e.url)});break;case"url":await w(e,SAVE_URL_QUERY,"saveUrl",{source:"extension",clientRequestId:n,url:encodeURI(e.url)});break;case"pdf":{const t=await function(e,t,n,o){return new Promise((s=>{const r=new XMLHttpRequest;r.onreadystatechange=async function(){if(4===r.readyState){if(200===r.status){const{data:t}=JSON.parse(r.response);if("errorCodes"in t.uploadFileRequest&&"UNAUTHORIZED"===t.uploadFileRequest.errorCodes[0]&&(b(),browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Unable to save page",type:"error",errorCode:401,url:c}})),t.uploadFileRequest&&t.uploadFileRequest.id&&t.uploadFileRequest.createdPageId&&!("errorCodes"in t.uploadFileRequest)){const r=await function({id:e,uploadSignedUrl:t},n,o){return fetch(o).then((e=>e.blob())).then((o=>new Promise((s=>{const r=new XMLHttpRequest;r.open("PUT",t,!0),r.setRequestHeader("Content-Type",n),r.onerror=()=>{s(void 0)},r.onload=()=>{s({id:e})},r.send(o)})))).catch((e=>{console.error("error uploading file",e)}))}(t.uploadFileRequest,n,o);if(URL.revokeObjectURL(o),!r)return;const a=t.uploadFileRequest.createdPageId,i=c+"/article/sr/"+a;return browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Saved to Omnivore",link:i,linkText:"Read Now",type:"success"}}),s(t.uploadFileRequest)}browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Unable to save page",type:"error"}})}else 400===r.status&&browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{text:"Unable to save page",type:"error"}});s(!1)}};const a=JSON.stringify({query:"mutation UploadFileRequest($input: UploadFileRequestInput!) {\n uploadFileRequest(input:$input) {\n ... on UploadFileRequestError {\n errorCodes\n }\n ... on UploadFileRequestSuccess {\n id\n createdPageId\n uploadSignedUrl\n }\n }\n }",variables:{input:{url:t,contentType:n,createPageEntry:!0}}});r.open("POST",d+"graphql",!0),g(r),r.send(a)}))}(e,encodeURI(e.url),s.contentType,a);if(!t||!t.id)return void await w(e,SAVE_URL_QUERY,"saveUrl",{source:"extension",clientRequestId:n,url:encodeURI(e.url)});break}}}))}async function A(e){const t=await l(e+"_saveInProgress");if(!t)return;clearInterval(t);const n=await l(e+"_saveInProgressTimeoutId_"+t);n&&clearTimeout(n)}function h(e){return l("postInstallClickComplete").then((async t=>{if(t)return!0;if("undefined"!=typeof browser&&browser.runtime&&browser.runtime.sendNativeMessage){const e=await browser.runtime.sendNativeMessage("omnivore",{message:ACTIONS.GetAuthToken});e.authToken&&(u=e.authToken)}return new Promise((t=>{const n=new XMLHttpRequest;n.onreadystatechange=function(){if(4===n.readyState&&200===n.status){const{data:o}=JSON.parse(n.response);o.me?(p({postInstallClickComplete:!0}),t(!0)):(browserApi.tabs.sendMessage(e,{action:ACTIONS.ShowMessage,payload:{type:"loading",text:"Loading..."}}),browserApi.tabs.sendMessage(e,{action:ACTIONS.ShowMessage,payload:{text:"",type:"error",errorCode:401,url:c}}),t(null))}};const o=JSON.stringify({query:"{me{id}}"});n.open("POST",d+"graphql",!0),g(n),n.send(o)}))}))}function S(e){new Promise((e=>{browserApi.tabs.query({active:!0,currentWindow:!0},(function(t){e(t[0]||null)}))})).then((t=>{browserApi.tabs.sendMessage(t.id,{action:ACTIONS.Ping},(async function(n){if(n&&n.pong)await h(t.id)&&e(t);else{const n=browserApi.runtime.getManifest().content_scripts,o=[...n[0].js,...n[1].js];!function(n,o,s){function r(e,t,n){return function(){browserScriptingApi.executeScript(e,t,n)}}let a=async function(){await h(t.id)&&e(t)};for(let e=o.length-1;e>=0;--e)a=r(n,{file:o[e]},a);null!==a&&a()}(t.id,o)}}))}))}function v(e,t){let n="/images/toolbar/icon";if(ENV_IS_FIREFOX?n+="_firefox":ENV_IS_EDGE&&(n+="_edge"),e||(n+="_inactive"),("boolean"==typeof t?t:window.matchMedia("(prefers-color-scheme: dark)").matches)&&(n+="_dark"),ENV_IS_FIREFOX)return n+".svg";const o=["16","24","32","48"];ENV_IS_EDGE||o.push("19","38");const s={};for(let e=0;e{!function t(){browserApi.tabs.get(e,(function(e){browserApi.runtime.lastError&&setTimeout(t,150),I(e)}))}()})),browserApi.tabs.onUpdated.addListener(((e,t,n)=>{t.status&&n&&n.active&&I(n)})),browserApi.tabs.onRemoved.addListener((e=>{!function(e){new Promise((e=>{browserApi.storage.local.get(null,(t=>{e(t||{})}))})).then((function(t){const n=[],o=Object.keys(t),s=e+"_saveInProgress";for(let e=0;e{"complete"!==e.status?(browserApi.tabs.sendMessage(e.id,{action:ACTIONS.ShowMessage,payload:{type:"loading",text:"Page loading..."}}),n&&"function"==typeof n&&n()):(t&&"function"==typeof t&&t(),y(e))}))}A(e),t(null,(()=>{!function(e,t,n,o=1e3,s=10500){const r=setInterval(e,o),a=setTimeout((()=>{clearInterval(r),t()}),s);n&&"function"==typeof n&&n(r,a)}((()=>{t((()=>{A(e)}))}),(()=>{A(e),browserApi.tabs.get(e,(e=>{y(e)}))}),((t,n)=>{const o={};o[e+"_saveInProgress"]=t,o[e+"_saveInProgressTimeoutId_"+t]=n,p(o)}))}))}(e.id)}))})),browserApi.runtime.onMessage.addListener(((e,t,n)=>{if(e.forwardToTab)return delete e.forwardToTab,void browserApi.tabs.sendRequest(t.tab.id,e);e.action===ACTIONS.RefreshDarkMode&&m(t.tab.id,e.payload.value)})),browserActionApi.setIcon({path:v(!0)}),browserApi.contextMenus.create({id:"save-selection",title:"Save to Omnivore",contexts:["link"],onclick:async function(e){S((async function(t){await async function(e,t){const n=i();await w(e,SAVE_URL_QUERY,"saveUrl",{source:"extension",clientRequestId:n,url:encodeURI(t)})}(t,e.linkUrl)}))}})})(); \ No newline at end of file diff --git a/apple/Sources/SafariExtension/Resources/scripts/constants.js b/apple/Sources/SafariExtension/Resources/scripts/constants.js index 86558e7ad..3e6593627 100644 --- a/apple/Sources/SafariExtension/Resources/scripts/constants.js +++ b/apple/Sources/SafariExtension/Resources/scripts/constants.js @@ -1 +1 @@ -"use strict";window.browserApi="object"==typeof chrome&&chrome&&chrome.runtime&&chrome||"object"==typeof browser&&browser||{},window.browserActionApi=browserApi.action||browserApi.browserAction||browserApi.pageAction,window.browserScriptingApi=browserApi.scripting||browserApi.tabs,window.ENV_EXTENSION_ORIGIN=browserApi.runtime.getURL("PATH/").replace("/PATH/",""),window.ENV_IS_FIREFOX=ENV_EXTENSION_ORIGIN.startsWith("moz-extension://"),window.ENV_IS_EDGE=navigator.userAgent.toLowerCase().indexOf("edg")>-1,window.ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS=/^((?!chrome|android).)*safari/i.test(navigator.userAgent),window.SELECTORS={CANONICAL_URL:["head > link[rel='canonical']"],TITLE:["head > meta[property='og:title']"]},window.ACTIONS={ShowMessage:"SHOW_MESSAGE",GetContent:"GET_CONTENT",GetPageInfo:"GET_PAGE_INFO",Ping:"PING",AddIframeContent:"ADD_IFRAME_CONTENT",RefreshDarkMode:"REFRESH_DARK_MODE",GetAuthToken:"GET_AUTH_TOKEN"},window.DONT_REMOVE_ELEMENTS=["meta","script","title"],window.CREATE_ARTICLE_QUERY="mutation CreateArticle ($input: CreateArticleInput!){\n createArticle(input:$input){\n ... on CreateArticleSuccess{\n createdArticle{\n id\n title\n slug\n hasContent\n }\n user {\n id\n profile {\n id\n username\n }\n }\n}\n ... on CreateArticleError{\n errorCodes\n }\n}\n}",window.CREATE_ARTICLE_SAVING_REQUEST_QUERY="mutation CreateArticleSavingRequest ($input: CreateArticleSavingRequestInput!){\n createArticleSavingRequest(input:$input){\n ... on CreateArticleSavingRequestSuccess{\n articleSavingRequest{\n id\n }\n }\n ... on CreateArticleSavingRequestError{\n errorCodes\n }\n }\n}"; \ No newline at end of file +"use strict";window.browserApi="object"==typeof chrome&&chrome&&chrome.runtime&&chrome||"object"==typeof browser&&browser||{},window.browserActionApi=browserApi.action||browserApi.browserAction||browserApi.pageAction,window.browserScriptingApi=browserApi.scripting||browserApi.tabs,window.ENV_EXTENSION_ORIGIN=browserApi.runtime.getURL("PATH/").replace("/PATH/",""),window.ENV_IS_FIREFOX=ENV_EXTENSION_ORIGIN.startsWith("moz-extension://"),window.ENV_IS_EDGE=navigator.userAgent.toLowerCase().indexOf("edg")>-1,window.ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS=/^((?!chrome|android).)*safari/i.test(navigator.userAgent),window.SELECTORS={CANONICAL_URL:["head > link[rel='canonical']"],TITLE:["head > meta[property='og:title']"]},window.ACTIONS={ShowMessage:"SHOW_MESSAGE",GetContent:"GET_CONTENT",GetPageInfo:"GET_PAGE_INFO",Ping:"PING",AddIframeContent:"ADD_IFRAME_CONTENT",RefreshDarkMode:"REFRESH_DARK_MODE",GetAuthToken:"GET_AUTH_TOKEN"},window.DONT_REMOVE_ELEMENTS=["meta","script","title"],window.SAVE_URL_QUERY="mutation SaveUrl ($input: SaveUrlInput!) {\n saveUrl(input:$input){\n ... on SaveSuccess {\n url\n }\n ... on SaveError {\n errorCodes\n }\n }\n}",window.SAVE_FILE_QUERY="mutation SaveFile ($input: SaveFileInput!) {\n saveFile(input:$input){\n ... on SaveSuccess {\n url\n }\n ... on SaveError {\n errorCodes\n }\n }\n}",window.SAVE_PAGE_QUERY="mutation SavePage ($input: SavePageInput!) {\n savePage(input:$input){\n ... on SaveSuccess {\n url\n }\n ... on SaveError {\n errorCodes\n }\n }\n}",window.CREATE_ARTICLE_QUERY="mutation CreateArticle ($input: CreateArticleInput!){\n createArticle(input:$input){\n ... on CreateArticleSuccess{\n createdArticle{\n id\n title\n slug\n hasContent\n }\n user {\n id\n profile {\n id\n username\n }\n }\n}\n ... on CreateArticleError{\n errorCodes\n }\n}\n}",window.CREATE_ARTICLE_SAVING_REQUEST_QUERY="mutation CreateArticleSavingRequest ($input: CreateArticleSavingRequestInput!){\n createArticleSavingRequest(input:$input){\n ... on CreateArticleSavingRequestSuccess{\n articleSavingRequest{\n id\n }\n }\n ... on CreateArticleSavingRequestError{\n errorCodes\n }\n }\n}"; \ No newline at end of file diff --git a/packages/api/src/services/save_file.ts b/packages/api/src/services/save_file.ts index c7248ef44..da3e32f4f 100644 --- a/packages/api/src/services/save_file.ts +++ b/packages/api/src/services/save_file.ts @@ -23,13 +23,11 @@ export const saveFile = async ( ): Promise => { console.log('saving file with input', input) - // /* We do not trust the values from client, lookup upload file by querying - // * with filtering on user ID and URL to verify client's uploadFileId is valid. - // */ const uploadFile = await ctx.models.uploadFile.getWhere({ id: input.uploadFileId, userId: saver.id, }) + if (!uploadFile) { return { errorCodes: [SaveErrorCode.Unauthorized], @@ -45,64 +43,6 @@ export const saveFile = async ( return ctx.models.uploadFile.setFileUploadComplete(input.uploadFileId, tx) }) - // if (!uploadFileData || !uploadFileData.id || !uploadFileData.fileName) { - // console.log('error completing upload file request', input) - // return { - // errorCodes: [SaveErrorCode.Unknown], - // } - // } - - // // const uploadFileUrlOverride = await makeStorageFilePublic( - // // uploadFileData.id, - // // uploadFileData.fileName - // // ) - - // const matchedUserArticleRecord = await getPageByParam({ - // userId: saver.id, - // url: uploadFileData.url, - // state: ArticleSavingRequestStatus.Succeeded, - // }) - - // if (matchedUserArticleRecord) { - // await updatePage( - // matchedUserArticleRecord.id, - // { - // savedAt: new Date(), - // archivedAt: null, - // }, - // ctx - // ) - // input.clientRequestId = matchedUserArticleRecord.id - // } else { - // const pageId = await createPage( - // { - // url: uploadFile.url, - // title: uploadFile.fileName, - // hash: uploadFileDetails.md5Hash, - // content: '', - // pageType: PageType.File, - // uploadFileId: input.uploadFileId, - // slug: generateSlug(uploadFile.fileName), - // userId: saver.id, - // id: input.clientRequestId, - // createdAt: new Date(), - // savedAt: new Date(), - // readingProgressPercent: 0, - // readingProgressAnchorIndex: 0, - // state: ArticleSavingRequestStatus.Succeeded, - // }, - // ctx - // ) - - // if (!pageId) { - // console.log('error creating page in elastic', input) - // return { - // errorCodes: [SaveErrorCode.Unknown], - // } - // } - // input.clientRequestId = pageId - // } - return { clientRequestId: input.clientRequestId, url: `${homePageURL()}/${saver.profile.username}/links/${ diff --git a/pkg/extension/.env.demo b/pkg/extension/.env.demo index cf435d0ed..6e22af7e0 100644 --- a/pkg/extension/.env.demo +++ b/pkg/extension/.env.demo @@ -1,3 +1,3 @@ OMNIVORE_URL="https://demo.omnivore.app" -OMNIVORE_GRAPHQL_URL="https://demo.omnivore.app/api/" +OMNIVORE_GRAPHQL_URL="https://api-demo.omnivore.app/api/" EXTENSION_NAME="Omnivore *DEMO*" diff --git a/pkg/extension/.env.production b/pkg/extension/.env.production index 71893c18f..939062848 100644 --- a/pkg/extension/.env.production +++ b/pkg/extension/.env.production @@ -1,3 +1,3 @@ OMNIVORE_URL="https://omnivore.app" -OMNIVORE_GRAPHQL_URL="https://omnivore.app/api/" +OMNIVORE_GRAPHQL_URL="https://api-prod.omnivore.app/api/" EXTENSION_NAME="Omnivore" diff --git a/pkg/extension/package.json b/pkg/extension/package.json index 514aef5d5..47e06ea77 100644 --- a/pkg/extension/package.json +++ b/pkg/extension/package.json @@ -22,5 +22,8 @@ "webpack": "^5.11.1", "webpack-cli": "^4.3.1", "webpack-merge": "^5.7.3" + }, + "dependencies": { + "uuid": "^8.3.2" } } diff --git a/pkg/extension/src/manifest.json b/pkg/extension/src/manifest.json index 17ff70de7..ced972368 100644 --- a/pkg/extension/src/manifest.json +++ b/pkg/extension/src/manifest.json @@ -22,7 +22,7 @@ "256": "/images/extension/icon-256.png" }, - "permissions": ["activeTab", "storage", "https://*/**", "http://*/**"], + "permissions": ["activeTab", "storage", "contextMenus", "https://*/**", "http://*/**"], "background": { "page": "/views/background.html", @@ -72,5 +72,14 @@ "default_title": "Omnivore Save Article" }, + "commands": { + "_execute_browser_action": { + "suggested_key": { + "default": "Alt + O" + }, + "description": "Save the current tab to Omnivore" + } + }, + "web_accessible_resources": ["views/cta-popup.html"] } diff --git a/pkg/extension/src/scripts/background.js b/pkg/extension/src/scripts/background.js index 0e9099bef..dade644d9 100644 --- a/pkg/extension/src/scripts/background.js +++ b/pkg/extension/src/scripts/background.js @@ -13,6 +13,8 @@ 'use strict'; +import { v4 as uuidv4 } from 'uuid'; + let authToken = undefined; const omnivoreURL = process.env.OMNIVORE_URL; const omnivoreGraphqlURL = process.env.OMNIVORE_GRAPHQL_URL; @@ -47,6 +49,17 @@ function removeStorage (itemsToRemove) { }); } +function getCurrentTab () { + return new Promise((resolve) => { + browserApi.tabs.query({ + active: true, + currentWindow: true + }, function (tabs) { + resolve(tabs[0] || null); + }); + }); +} + function setupConnection(xhr) { xhr.setRequestHeader('Content-Type', 'application/json'); if (authToken) { @@ -65,7 +78,7 @@ function uploadFile ({ id, uploadSignedUrl }, contentType, contentObjUrl) { xhr.setRequestHeader('Content-Type', contentType); xhr.onerror = () => { - resolve(null); + resolve(undefined); }; xhr.onload = () => { // Uploaded. @@ -80,14 +93,13 @@ function uploadFile ({ id, uploadSignedUrl }, contentType, contentObjUrl) { }); } -function savePdfFile (tab, url, contentType, contentObjUrl) { +function savePdfFile(tab, url, contentType, contentObjUrl) { return new Promise(resolve => { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = async function () { if (xhr.readyState === 4) { if (xhr.status === 200) { const { data } = JSON.parse(xhr.response); - if ('errorCodes' in data.uploadFileRequest) { if (data.uploadFileRequest.errorCodes[0] === 'UNAUTHORIZED') { clearClickCompleteState(); @@ -103,7 +115,7 @@ function savePdfFile (tab, url, contentType, contentObjUrl) { } } - if (!data.uploadFileRequest || !data.uploadFileRequest.id || 'errorCodes' in data.uploadFileRequest) { + if (!data.uploadFileRequest || !data.uploadFileRequest.id || !data.uploadFileRequest.createdPageId || 'errorCodes' in data.uploadFileRequest) { browserApi.tabs.sendMessage(tab.id, { action: ACTIONS.ShowMessage, payload: { @@ -112,9 +124,26 @@ function savePdfFile (tab, url, contentType, contentObjUrl) { } }); } else { - const uploadFileId = await uploadFile(data.uploadFileRequest, contentType, contentObjUrl); + const result = await uploadFile(data.uploadFileRequest, contentType, contentObjUrl); URL.revokeObjectURL(contentObjUrl); - return resolve(uploadFileId); + + if (!result) { + return undefined + } + + const createdPageId = data.uploadFileRequest.createdPageId + const url = omnivoreURL + '/article/sr/' + createdPageId + + browserApi.tabs.sendMessage(tab.id, { + action: ACTIONS.ShowMessage, + payload: { + text: 'Saved to Omnivore', + link: url, + linkText: 'Read Now', + type: 'success' + } + }) + return resolve(data.uploadFileRequest); } } else if (xhr.status === 400) { browserApi.tabs.sendMessage(tab.id, { @@ -137,6 +166,7 @@ function savePdfFile (tab, url, contentType, contentObjUrl) { } ... on UploadFileRequestSuccess { id + createdPageId uploadSignedUrl } } @@ -144,7 +174,8 @@ function savePdfFile (tab, url, contentType, contentObjUrl) { variables: { input: { url, - contentType + contentType, + createPageEntry: true, } } }); @@ -164,8 +195,71 @@ function clearClickCompleteState () { }); } -function saveArticle (tab) { - browserApi.tabs.sendMessage(tab.id, { +function handleSaveResponse(tab, field, xhr) { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + const { data } = JSON.parse(xhr.response); + const item = data[field] + if (!item) { + return undefined + } + + if ('errorCodes' in item) { + const messagePayload = { + text: descriptions[data.createArticle.errorCodes[0]] || 'Unable to save page', + type: 'error' + } + + if (item.errorCodes[0] === 'UNAUTHORIZED') { + messagePayload.errorCode = 401 + messagePayload.url = omnivoreURL + clearClickCompleteState() + } + + browserApi.tabs.sendMessage(tab.id, { + action: ACTIONS.ShowMessage, + payload: messagePayload + }) + + return undefined + } + + const url = item['url'] + browserApi.tabs.sendMessage(tab.id, { + action: ACTIONS.ShowMessage, + payload: { + text: 'Saved to Omnivore', + link: url ?? omnivoreURL + '/home', + linkText: 'Read Now', + type: 'success' + } + }) + + return item + } else if (xhr.status === 400) { + browserApi.tabs.sendMessage(tab.id, { + action: ACTIONS.ShowMessage, + payload: { + text: 'Unable to save page', + type: 'error' + } + }) + return undefined + } + } +} + +async function saveUrl(currentTab, url) { + const requestId = uuidv4() + await saveApiRequest(currentTab, SAVE_URL_QUERY, 'saveUrl', { + source: 'extension', + clientRequestId: requestId, + url: encodeURI(url), + }) +} + +async function saveApiRequest(currentTab, query, field, input) { + browserApi.tabs.sendMessage(currentTab.id, { action: ACTIONS.ShowMessage, payload: { type: 'loading', @@ -173,85 +267,48 @@ function saveArticle (tab) { } }); - const xhr = new XMLHttpRequest(); - const descriptions = { - BAD_DATA: 'Unable to save page', - NOT_ALLOWED_TO_PARSE: 'Not allowed to parse this article', - UNAUTHORIZED: 'Please login to Omnivore to authorize this action', - UNABLE_TO_FETCH: 'Unable to fetch page', - PAYLOAD_TOO_LARGE: 'This article is too large' - }; - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - const { data } = JSON.parse(xhr.response); - if ('createArticle' in data) { - if ('errorCodes' in data.createArticle) { - const messagePayload = { - text: descriptions[data.createArticle.errorCodes[0]] || 'Unable to save page', - type: 'error' - }; + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + xhr.open('POST', omnivoreGraphqlURL + 'graphql', true) + setupConnection(xhr) - if (data.createArticle.errorCodes[0] === 'UNAUTHORIZED') { - messagePayload.errorCode = 401; - messagePayload.url = omnivoreURL; + xhr.onerror = (err) => { reject(err) } - clearClickCompleteState(); - } - - browserApi.tabs.sendMessage(tab.id, { - action: ACTIONS.ShowMessage, - payload: messagePayload - }); - } else { - const article = data.createArticle.createdArticle; - const user = data.createArticle.user; - const link = omnivoreURL + (article.hasContent ? (`/${user.profile.username}/` + article.slug) : '/home'); - browserApi.tabs.sendMessage(tab.id, { - action: ACTIONS.ShowMessage, - payload: { - text: 'Saved to Omnivore', - link: link, - linkText: 'View', - type: 'success' - } - }); - } - } else { - if ('errorCodes' in data.createArticleSavingRequest) { - browserApi.tabs.sendMessage(tab.id, { - action: ACTIONS.ShowMessage, - payload: { - text: descriptions[data.createArticleSavingRequest.errorCodes[0]] || 'Unable to save page', - type: 'error' - } - }); - } else { - const articleSavingRequest = data.createArticleSavingRequest.articleSavingRequest; - const link = omnivoreURL + '/article/sr/' + articleSavingRequest.id; - browserApi.tabs.sendMessage(tab.id, { - action: ACTIONS.ShowMessage, - payload: { - text: 'Saved to Omnivore', - link: link, - linkText: 'View', - type: 'success' - } - }); - } + xhr.onload = () => { + try { + const res = handleSaveResponse(currentTab, field, xhr) + if (!res) { + return reject() } - } else if (xhr.status === 400) { - browserApi.tabs.sendMessage(tab.id, { - action: ACTIONS.ShowMessage, - payload: { - text: 'Unable to save page', - type: 'error' - } - }); + resolve(res); + } catch (err) { + reject(err) } } - }; + const data = JSON.stringify({ + query, + variables: { + input + } + }) + + xhr.send(data); + }) + .catch((err) => { + console.log('error saving page', err) + browserApi.tabs.sendMessage(currentTab.id, { + action: ACTIONS.ShowMessage, + payload: { + text: 'Unable to save page', + type: 'error', + } + }); + return undefined + }); +} + +function saveArticle (tab) { browserApi.tabs.sendMessage(tab.id, { action: ACTIONS.GetContent }, async (response) => { @@ -260,56 +317,42 @@ function saveArticle (tab) { return; } + const requestId = uuidv4() const { type, pageInfo, doc, uploadContentObjUrl } = response; - let uploadResult = null; - switch(type) { - case 'pdf': { - // For PDFs, we first upload the PDF file before passing the upload file ID in createArticle - uploadResult = await savePdfFile(tab, encodeURI(tab.url), pageInfo.contentType, uploadContentObjUrl); - if (!uploadResult || !uploadResult.id) { - browserApi.tabs.sendMessage(tab.id, { - action: ACTIONS.ShowMessage, - payload: { - text: 'Unable to save page', - type: 'error' - } - }); - return; - } + case 'html': { + await saveApiRequest(tab, SAVE_PAGE_QUERY, 'savePage', { + source: 'extension', + clientRequestId: requestId, + originalContent: doc, + title: pageInfo.title, + url: encodeURI(tab.url), + }) + break } case 'url': { - // We don't have to special case URL, it will fall through - // and be handled when isContentAvailable returns false + await saveApiRequest(tab, SAVE_URL_QUERY, 'saveUrl', { + source: 'extension', + clientRequestId: requestId, + url: encodeURI(tab.url), + }) + break + } + case 'pdf': { + const uploadResult = await savePdfFile(tab, encodeURI(tab.url), pageInfo.contentType, uploadContentObjUrl); + if (!uploadResult || !uploadResult.id) { + // If the upload failed for any reason, try to save the PDF URL instead + await saveApiRequest(tab, SAVE_URL_QUERY, 'saveUrl', { + source: 'extension', + clientRequestId: requestId, + url: encodeURI(tab.url), + }) + return; + } + break } } - - const isContentAvailable = (doc && doc.length) || uploadResult; - const query = isContentAvailable ? CREATE_ARTICLE_QUERY : CREATE_ARTICLE_SAVING_REQUEST_QUERY; - const input = { - url: encodeURI(tab.url) - }; - - if (isContentAvailable) { - input.preparedDocument = { - document: doc, - pageInfo - }; - input.uploadFileId = (uploadResult && uploadResult.id) || null; - } - - const data = JSON.stringify({ - query, - variables: { - input - } - }); - - xhr.open('POST', omnivoreGraphqlURL + 'graphql', true); - setupConnection(xhr); - - xhr.send(data); }); } @@ -484,18 +527,13 @@ function checkAuthOnFirstClickPostInstall (tabId) { }); } -function getCurrentTab () { - return new Promise((resolve) => { - browserApi.tabs.query({ - active: true, - currentWindow: true - }, function (tabs) { - resolve(tabs[0] || null); - }); - }); +function handleActionClick () { + executeAction(function (currentTab) { + onExtensionClick(currentTab.id); + }) } -function handleActionClick () { +function executeAction(action) { getCurrentTab().then((currentTab) => { browserApi.tabs.sendMessage(currentTab.id, { action: ACTIONS.Ping @@ -504,7 +542,7 @@ function handleActionClick () { // Content script ready const isSignedUp = await checkAuthOnFirstClickPostInstall(currentTab.id); if (isSignedUp) { - onExtensionClick(currentTab.id); + action(currentTab); } } else { const extensionManifest = browserApi.runtime.getManifest(); @@ -518,7 +556,7 @@ function handleActionClick () { executeScripts(currentTab.id, scriptFiles, async function () { const isSignedUp = await checkAuthOnFirstClickPostInstall(currentTab.id); if (isSignedUp) { - onExtensionClick(currentTab.id); + action(currentTab); } }); } @@ -644,6 +682,17 @@ function init () { browserActionApi.setIcon({ path: getIconPath(true) }); + + browserApi.contextMenus.create({ + id: "save-selection", + title: "Save to Omnivore", + contexts: ["link"], + onclick: async function(obj) { + executeAction(async function (currentTab) { + await saveUrl(currentTab, obj.linkUrl) + }) + }, + }); } init(); diff --git a/pkg/extension/src/scripts/constants.js b/pkg/extension/src/scripts/constants.js index 596447c31..4d169fdcf 100644 --- a/pkg/extension/src/scripts/constants.js +++ b/pkg/extension/src/scripts/constants.js @@ -30,6 +30,39 @@ window.DONT_REMOVE_ELEMENTS = [ 'title' ]; +window.SAVE_URL_QUERY = `mutation SaveUrl ($input: SaveUrlInput!) { + saveUrl(input:$input){ + ... on SaveSuccess { + url + } + ... on SaveError { + errorCodes + } + } +}` + +window.SAVE_FILE_QUERY = `mutation SaveFile ($input: SaveFileInput!) { + saveFile(input:$input){ + ... on SaveSuccess { + url + } + ... on SaveError { + errorCodes + } + } +}` + +window.SAVE_PAGE_QUERY = `mutation SavePage ($input: SavePageInput!) { + savePage(input:$input){ + ... on SaveSuccess { + url + } + ... on SaveError { + errorCodes + } + } +}` + window.CREATE_ARTICLE_QUERY = `mutation CreateArticle ($input: CreateArticleInput!){ createArticle(input:$input){ ... on CreateArticleSuccess{ diff --git a/pkg/extension/src/scripts/content/content-listener-script.js b/pkg/extension/src/scripts/content/content-listener-script.js index 523f4d239..8fc3f0b73 100644 --- a/pkg/extension/src/scripts/content/content-listener-script.js +++ b/pkg/extension/src/scripts/content/content-listener-script.js @@ -22,7 +22,6 @@ } browserApi.runtime.onMessage.addListener(({ action, payload }, sender, sendResponse) => { - /* async actions */ if (action === ACTIONS.GetContent) { prepareContent().then((pageContent) => { sendResponse({ diff --git a/pkg/extension/yarn.lock b/pkg/extension/yarn.lock index aab637b0b..0af7516c1 100644 --- a/pkg/extension/yarn.lock +++ b/pkg/extension/yarn.lock @@ -4655,7 +4655,7 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0: +uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==