diff --git a/README.md b/README.md index d39f859..f10b671 100644 --- a/README.md +++ b/README.md @@ -122,10 +122,11 @@ This extension only communicates to the Chhoto URL server instance you configure ## Acknowledgements This project was inspired by and modified from Edward Shen's [Shlink extension][shlink-extension]. Modifications include: 1. Rewriting the backend code to contact a Chhoto URL server, rather than a Shlink server. -2. Changing the appearance of the `options.html` page. -3. Removing options which were either not possible to implement (because of the Chhoto URL server's limitations), or were not practical. -4. Optimizing code where possible. -5. Et cetera +2. Adding a popup page for manual URL generation. +3. Changing the appearance of the `options.html` page. +4. Removing options which were either not possible to implement (because of the Chhoto URL server's limitations), or were not practical. +5. Optimizing code where possible. +6. Et cetera. ## Information | Syncing from (main repository) | Syncing to | Syncing every | diff --git a/background/background.js b/background/background.js index 6786baf..d3b6ba5 100644 --- a/background/background.js +++ b/background/background.js @@ -51,7 +51,10 @@ * @type {object} * @property {number} status * @property {{success: boolean, error: boolean, shorturl: string}} json - * @property {string} requestedLink The link that tried to be created. This only activates, and is only accurate, if the generateWithTitle option is enabled. + * @property {string} host The host of the server. + * @property {string} requestedLink The link that tried to be created. This only activates, and is only accurate, if the generateWithTitle option is enabled or the type of the request is "popup" + * @property {string} type The type of the request. This can be "background", which means the request came from the background page. + * This can also be "popup", which means the request came from the popup page. */ /** @@ -143,16 +146,20 @@ function generateChhotoRequest([url, title, type]) { data.title = ""; } + // Set the type of the request + data.type = type; + // If "generateWithTitle" is true if (data.generateWithTitle) { // Get the configured word limit let wordLimit = data.titleWordLimit; // Format title name + // Trim all the whitespaces from the beginning and end of the string // Replace all occurences of ' - ' to '-' // Replace all occurences of ' ' to '-' // Replace all characters except for 'a-z', '0-9', '-' and '_' with '' - let titleName = title.toLowerCase().replace(/ - /g, '-').replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, ''); + let titleName = title.trim().toLowerCase().replace(/ - /g, '-').replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, ''); // If the wordLimit is not 0, and thus limited // If the type of the request does not equal "popup" (the word limit is always unlimited if it's generated from the popup page) @@ -195,8 +202,8 @@ function requestChhoto(chhotoRequest) { longlink: chhotoRequest.longUrl, }), }, - // Add the HTTP status code, and requestedLink (see ChhotoResponse in type definitions for details) to the response - )).then(r => r.json().then(data => ({ status: r.status, json: data, requestedLink: `${chhotoRequest.chhotoHost}/${chhotoRequest.title}` }))) + // Add the HTTP status code, type of the request, and requestedLink (see ChhotoResponse in type definitions for details) to the response + )).then(r => r.json().then(data => ({ status: r.status, json: data, host: chhotoRequest.chhotoHost, requestedLink: chhotoRequest.title, type: chhotoRequest.type }))) // If there was a HTTP error // This does not activate if the Chhoto server returns a JSON response with an error .catch(err => { @@ -225,11 +232,26 @@ function validateChhotoResponse(httpResp) { if (httpResp.json.success) { return httpResp.json; } else { + // If there was a URL conflict if (httpResp.status === 409) { + // If the type is popup + if (httpResp.type === "popup") { + // Define the error + const error = `Error: The URL "${httpResp.host}/${httpResp.requestedLink}" already exists.`; + + // Send a message to the popup + browser.runtime.sendMessage({type: "url-conflict", errorMessage: error, host: httpResp.host, shorturl: httpResp.requestedLink}); + + // Return an error + return Promise.reject(new Error( + error + )); + }; + const json = { success: true, error: false, - shorturl: httpResp.requestedLink, + shorturl: `${httpResp.host}/${httpResp.requestedLink}`, } return json; } @@ -243,11 +265,14 @@ function validateChhotoResponse(httpResp) { /** * Copies the returned shortened link to the clipboard. * - * @param {!ChhotoJSON} chhotoResp The JSON response from a Chhoto URL instance. + * @param {!ChhotoJSON} chhotoResp The JSON response from a Chhoto URL instance. **May also include "type"** if the request came from the popup.js script. * @returns {!Promise} `ChhotoJSON`, unmodified, on * success, or an error indicating that we failed to copy to the clipboard. */ function copyLinkToClipboard(chhotoResp) { + // Send finished message + browser.runtime.sendMessage({message: "finished"}); + // Chrome requires this hacky workaround :( if (typeof chrome !== "undefined") { const prevSelected = document.activeElement; diff --git a/popup/popup.html b/popup/popup.html index a2af4a7..01095d5 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -11,6 +11,7 @@ height: 300px; font-size: 20px; font-family: sans-serif; + margin-bottom: 20px; } .header { @@ -34,7 +35,9 @@ cursor: pointer; } - .generate-button { + .generate-button, + #url-conflict-yes, + #url-conflict-no { border: black 1px solid; border-radius: 7px; background-color: white; @@ -45,7 +48,9 @@ } #close:hover, - .generate-button:hover { + .generate-button:hover, + #url-conflict-yes:hover, + #url-conflict-no:hover { background-color: #bbb; } @@ -72,7 +77,9 @@ #message, #message2, - #message3 { + #message3, + #url-conflict, + #delete-error { display: none; } @@ -119,6 +126,19 @@ +
+

+

Would you like to replace it?

+ + +

If you replace a link, your browser may still cache the old destination. Others will be able to see the correct destination, if their browser didn't do the same.

+
+ +
+

Error: Failed to replace link

+

+
+ diff --git a/popup/popup.js b/popup/popup.js index 8def176..4e38204 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -36,14 +36,209 @@ const messageEle = document.querySelector("#message"); const message2Ele = document.querySelector("#message2"); const message3Ele = document.querySelector("#message3"); const generateEle = document.querySelector("#generate"); +const urlConflictEle = document.querySelector("#url-conflict"); +const urlConflictHeaderEle = document.querySelector("#url-conflict-header"); +const urlConflictYesEle = document.querySelector("#url-conflict-yes"); +const urlConflictNoEle = document.querySelector("#url-conflict-no"); +const deleteErrorEle = document.querySelector("#delete-error"); +const deleteErrorMessageEle = document.querySelector("#delete-error-message"); // Define values let shorturl; let longurl; +// Get the passed long URL, if present const requestParams = new URLSearchParams(window.location.search); const requestValue = requestParams.get('url'); +// Get the background page, and call the sendRequest function (which is in this script) +const backgroundFunc = browser.runtime.getBackgroundPage(); + +/** + * Functions + */ + +/** + * Closing the popup + */ + +// Close function +async function close() { + try { + const windowId = (await browser.windows.getCurrent()).id; + await browser.windows.remove(windowId); + } catch (error) { + console.log("Closing failed:", error); + }; +}; + +/** + * Send requests to the background page + */ + +// Define the sendRequest function (which calls a background.js function to generate a shortened link) +function sendRequest(page) { + page.popupGenerateChhoto(longurl, shorturl); +}; + +// Handle errors for sendRequest() function +function onError(error) { + console.log(`Error: ${error}`); +} + +/** + * Random string generation (to append to the shorturl, when requested) + */ + +// Define the "generate()" function +// This will generate a random string, if requested +function generate() { + // Define the alphabet + // The letters "cfhistu" are commonly used in rude words, so they are omitted + const ALPHABET = '0123456789abdegjklmnopqrvwxyz'; + const ID_LENGTH = 8; + + // Generate + let rtn = ""; + for (let i = 0; i < ID_LENGTH; i++) { + rtn += ALPHABET.charAt(Math.floor(Math.random() * ALPHABET.length)); + }; + + // Return the string + return rtn; +} + +/** + * Link deletion / link replacement + */ + +// Delete the link, and then make another request to the server to generate it again +// In other words, replace the link +function deleteLink(host, link) { + browser.storage.local.get("chhotoKey").then((chhotoKey) => { + const headers = new Headers(); + headers.append("accept", "application/json"); + headers.append("Content-Type", "application/json"); + // This has been pushed to the main branch of Chhoto URL! + headers.append("X-API-Key", chhotoKey.chhotoKey); + + // Fetch + return fetch(new Request( + `${host}/api/del/${link}`, + { + method: "DELETE", + headers, + }, + )).then(r => r.json().then(data => ({ status: r.status, json: data }))) + .then(result => { + if (result.json.success) { + // Remove error class, if applied + deleteErrorEle.classList.remove("warning"); + + // Send request to function which handles requests to the background page + backgroundFunc.then(sendRequest, onError); + } else if (result.json.error) { + // Set error message + deleteErrorMessageEle.innerText = `Error (${result.status}): ${result.json.reason}`; + + // Add warning style + deleteErrorEle.classList.add("warning"); + + }; + + }) + .catch(err => { + // Set error message + deleteErrorMessageEle.innerText = `Error (${result.status}): ${result.json.reason}`; + + // Add warning style + deleteErrorEle.classList.add("warning"); + + // Error + return Promise.reject(new Error( + `Error: ${err.message}` + )); + }); + }); +}; + +/** + * Listening to runtime messages + */ + +// Handle the messages (i.e. errors) +function handleMessage(request) { + // If the error was a URL conflict + if (request.type && request.type === "url-conflict") { + // Update the error message + urlConflictHeaderEle.innerText = request.errorMessage; + + // Add warning class + urlConflictEle.classList.add("warning"); + + // When the user clicks "yes" + urlConflictYesEle.onclick = () => { + // Call the deleteLink function + deleteLink(request.host, request.shorturl); + }; + + // When the user clicks "no" + urlConflictNoEle.onclick = () => { + // Reassign the short URL + shorturl = shorturl + "-" + generate(); + + // Update the value of the input field + shortURLEle.value = shorturl; + + // Remove the warning class + urlConflictEle.classList.remove("warning"); + }; + } else if (request.message === "finished") { + close(); + }; +}; + + + + + +/** + * Event listeners + * */ + +// If the close button is clicked, close the window +closeEle.addEventListener('click', () => { + close(); +}); + +// Listen for messages (i.e. errors) +browser.runtime.onMessage.addListener(handleMessage); + +// If the generate button was clicked +generateEle.addEventListener("submit", (event) => { + event.preventDefault(); + // Ensure both fields have been filled out, and the long URL is valid + if ( shorturl !== undefined && longurl !== undefined && !message2Ele.classList.contains("warning") ) { + + // Remove the warning class + message3Ele.classList.remove("warning"); + + // Send request to function which handles requests to the background page + backgroundFunc.then(sendRequest, onError); + } else { + // Add the warning class + message3Ele.classList.add("warning"); + }; +}); + + + + + +/** + * If statements, i.e. the main functionality + * */ + /** * Automatically insert the long URL, if enabled */ @@ -90,20 +285,10 @@ if (requestValue) { }); }; -// Close function -async function close() { - try { - const windowId = (await browser.windows.getCurrent()).id; - await browser.windows.remove(windowId); - } catch (error) { - console.log("Closing failed:", error); - } -} -// If the close button is clicked, close the window -closeEle.addEventListener('click', () => { - close(); -}); +/** + * When the short URL is inputted + * */ // When the short URL is inputted shortURLEle.oninput = (event) => { @@ -111,6 +296,13 @@ shortURLEle.oninput = (event) => { event.preventDefault(); }; + // Hide the URL conflict error, if it is present + urlConflictEle.classList.remove("warning"); + + // Hide the deletion error, if it is present + deleteErrorEle.classList.remove("warning"); + + // If the short URL value is greater than 0 (i.e. not an empty string) if (shortURLEle.value.length > 0) { // Get the browser storage @@ -122,10 +314,12 @@ shortURLEle.oninput = (event) => { const chhotoHost = data.chhotoHost; // Format the short URL + // Trim all the whitespaces from the beginning and end of the string // Replace all occurences of ' - ' to '-' // Replace all occurences of ' ' to '-' // Replace all characters except for 'a-z', '0-9', '-' and '_' with '' - let shortURLText = shortURLEle.value.toLowerCase().replace(/ - /g, '-').replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, ''); + let shortURLText = shortURLEle.value.trim().toLowerCase().replace(/ - /g, '-').replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, ''); + // If the wordLimit is not 0, and thus limited // And if the configured host is not undefined @@ -146,10 +340,13 @@ shortURLEle.oninput = (event) => { } else { // If the short URL is an empty string, hide the message messageEle.classList.remove("shown"); - } - + }; }; +/** + * When the long URL is inputted + * */ + // When the long URL is inputted longURLEle.oninput = (event) => { if (event.type === "click") { @@ -195,8 +392,6 @@ longURLEle.oninput = (event) => { message2Ele.classList.add("warning"); longURLEle.style.color = "red"; }; - - }); } else { // If the long URL is an empty string, hide the warning styling @@ -204,27 +399,3 @@ longURLEle.oninput = (event) => { longURLEle.style.color = "black"; } }; - -// Define the sendRequest function (which calls a background.js function to generate a shortened link) -function sendRequest(page) { - page.popupGenerateChhoto(longurl, shorturl); - close(); -} - -// If the generate button was clicked -generateEle.addEventListener("submit", (event) => { - event.preventDefault(); - // Ensure both fields have been filled out, and the long URL is valid - if ( shorturl !== undefined && longurl !== undefined && !message2Ele.classList.contains("warning") ) { - - // Remove the warning class - message3Ele.classList.remove("warning"); - - // Get the background page, and call the sendRequest function (which is in this script) - const backgroundFunc = browser.runtime.getBackgroundPage(); - backgroundFunc.then(sendRequest); - } else { - // Add the warning class - message3Ele.classList.add("warning"); - }; -});