mirror of
https://github.com/SolninjaA/Chhoto-URL-Extension.git
synced 2025-02-27 18:29:33 +10:00
376 lines
13 KiB
JavaScript
376 lines
13 KiB
JavaScript
/*
|
|
* An unofficial extension for easy link shortening using the Chhoto URL API.
|
|
* Copyright (C) 2025 Solomon Tech
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
|
|
/**
|
|
* File Overview
|
|
* @file This file actually creates a shortened link. "options.js" is only for the options page, "popup.js" calles this script to generate a shortened link - this file handles the main extension functionality
|
|
* @copyright Solomon Tech 2025
|
|
*/
|
|
|
|
|
|
// Use JavaScript's "strict" mode
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode
|
|
"use strict";
|
|
|
|
/*
|
|
* Type Definitions
|
|
*/
|
|
|
|
/**
|
|
* @typedef ChhotoRequest
|
|
* @type {object}
|
|
* @property {string[]} allowedProtocols The protocols which are allowed when creating a shortened link. If all are disabled, every protocol is allowed.
|
|
* @property {string} chhotoHost The location of the Chhoto URL instance.
|
|
* @property {string} chhotoKey The API key to communicate with a Chhoto URL instance with.
|
|
* @property {string} longUrl The requested URL to shorten.
|
|
* @property {string} title The title of the website
|
|
*/
|
|
|
|
/**
|
|
* This is a subset of the full API response from Chhoto URL.
|
|
* If the server returns an error instead, the response is slightly different.
|
|
*
|
|
* @see {@link https://github.com/SinTan1729/chhoto-url?tab=readme-ov-file#instructions-for-cli-usage} for more information.
|
|
* @typedef ChhotoResponse
|
|
* @type {object}
|
|
* @property {number} status
|
|
* @property {{success: boolean, error: boolean, shorturl: string}} json
|
|
* @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.
|
|
*/
|
|
|
|
/**
|
|
* The JSON data obtained from the ChhotoResponse data (see above).
|
|
* If the server returns an error instead, the response is slightly different.
|
|
*
|
|
* @typedef ChhotoJSON
|
|
* @type {object}
|
|
* @property {boolean} success
|
|
* @property {boolean} error
|
|
* @property {string} shorturl
|
|
*/
|
|
|
|
/*
|
|
* Functions - in order from first executed to last executed by generateChhoto()
|
|
*/
|
|
|
|
/**
|
|
* Validates the URL, as in, checks if the protocol is allowed.
|
|
*
|
|
* @param {!URL} url
|
|
* @param {!string} title
|
|
* @param {!string} type
|
|
* @returns {!Promise<[URL, string, string], Error>}
|
|
*/
|
|
function validateURL(url, title, type) {
|
|
return browser.storage.local.get("allowedProtocols").then(({ allowedProtocols }) => {
|
|
// Initialize a list of protocols that are allowed if unset.
|
|
// This needs to be synced with the initialization code in options.js.
|
|
if (allowedProtocols === undefined) {
|
|
allowedProtocols = new Set();
|
|
allowedProtocols.add("http:");
|
|
allowedProtocols.add("https:");
|
|
allowedProtocols.add("ftp:");
|
|
allowedProtocols.add("file:");
|
|
browser.storage.local.set({ allowedProtocols: Array(...allowedProtocols) });
|
|
} else {
|
|
allowedProtocols = new Set(allowedProtocols);
|
|
}
|
|
|
|
// If no protocols are set, allow every protocol
|
|
if (allowedProtocols.size > 0 && !allowedProtocols.has(url.protocol)) {
|
|
return Promise.reject(new Error(`The current page's protocol (${url.protocol}) is unsupported.`));
|
|
}
|
|
|
|
// Return URL and title
|
|
return Promise.resolve([url, title, type]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parses the URL outputted in the previous function, and gets the full link (i.e. URI included).
|
|
*
|
|
* @param {[!URL, string, string]} url - Holds long URL
|
|
* title - Title of the website
|
|
* type - Holds the type of the request. If this is "background", the function was called from this script.
|
|
* If this is "popup", the function was called from the popup script.
|
|
* @returns {!Promise<ChhotoRequest, Error>}
|
|
* If all the data was obtained, return ChhotoRequest.
|
|
* Else, return an error
|
|
*/
|
|
function generateChhotoRequest([url, title, type]) {
|
|
return browser.storage.local.get().then((data) => {
|
|
// If the user didn't specify an API key or a host
|
|
if (!data.chhotoHost) {
|
|
return Promise.reject(new Error(
|
|
"Missing Chhoto URL host. Please configure the Chhoto URL extension. See https://git.solomon.tech/solomon/Chhoto-URL-Extension#installation for more information."
|
|
));
|
|
}
|
|
|
|
// If the user didn't specify an API key
|
|
if (!data.chhotoKey) {
|
|
return Promise.reject(new Error(
|
|
"Missing API Key. Please configure the Chhoto URL extension. See https://git.solomon.tech/solomon/Chhoto-URL-Extension#installation for more information."
|
|
));
|
|
}
|
|
|
|
|
|
// Set URL and title
|
|
data.longUrl = url.href;
|
|
|
|
// If the request type is popup
|
|
if (type === "popup") {
|
|
data.title = title;
|
|
// Normal type (i.e. generated by clicking on the extension icon once)
|
|
} else {
|
|
// Make the title an empty string by default
|
|
// This will create a randomly generated string if sent to the Chhoto URL server
|
|
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.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)
|
|
if (wordLimit !== "0" && type !== "popup") {
|
|
// Limit the length of the short URL to the configured number
|
|
titleName = titleName.split('-', wordLimit).join('-');
|
|
}
|
|
|
|
// Set the title
|
|
data.title = titleName;
|
|
};
|
|
|
|
// Return
|
|
return Promise.resolve(data);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches a response from the Chhoto URL instance using the provided arguments.
|
|
*
|
|
* @param {!ChhotoRequest} chhotoRequest An object containing all the variables
|
|
* needed to request a shortened link from a Chhoto URL instance.
|
|
* @returns {!Promise<ChhotoResponse, Error>} The HTTP response from the Chhoto URL instance, or an error
|
|
*/
|
|
function requestChhoto(chhotoRequest) {
|
|
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", chhotoRequest.chhotoKey);
|
|
|
|
// Return output of fetch
|
|
return fetch(new Request(
|
|
`${chhotoRequest.chhotoHost}/api/new`,
|
|
{
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({
|
|
shortlink: chhotoRequest.title,
|
|
longlink: chhotoRequest.longUrl,
|
|
}),
|
|
},
|
|
// 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 => {
|
|
// Change the error message, if there was a NetworkError
|
|
if (err.message === "NetworkError when attempting to fetch resource.") {
|
|
err.message = "Failed to access the Chhoto URL instance. Is the instance online?"
|
|
};
|
|
|
|
return Promise.reject(new Error(
|
|
`Error: ${err.message}`
|
|
));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checks if the HTTP response was valid.
|
|
*
|
|
* @param {!ChhotoResponse} httpResp The response from the Chhoto URL instance from
|
|
* requesting a shortened link.
|
|
* @returns {!Promise<ChhotoJSON, Error>} An object containing the server
|
|
* response if the server responded successfully, or an error describing the
|
|
* HTTP error code returned by the server.
|
|
*/
|
|
function validateChhotoResponse(httpResp) {
|
|
// Rather than check the HTTP status code, check the response of the Chhoto URL instance.
|
|
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.host}/${httpResp.requestedLink}`,
|
|
}
|
|
return json;
|
|
}
|
|
|
|
return Promise.reject(new Error(
|
|
`Error (${httpResp.status}): ${httpResp.json.reason}.`
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies the returned shortened link to the clipboard.
|
|
*
|
|
* @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, Error>} `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;
|
|
const tempEle = document.createElement("input");
|
|
document.body.appendChild(tempEle);
|
|
tempEle.value = chhotoResp.shorturl;
|
|
tempEle.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(tempEle);
|
|
// Depending on what was previously selected, we might not be able to select the text.
|
|
if (prevSelected?.select) {
|
|
prevSelected.select();
|
|
}
|
|
return Promise.resolve(chhotoResp);
|
|
} else {
|
|
return navigator.clipboard
|
|
.writeText(chhotoResp.shorturl)
|
|
.then(
|
|
() => Promise.resolve(chhotoResp),
|
|
(e) => Promise.reject(new Error(`Failed to copy to clipboard. ${e.message}`))
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a success notification.
|
|
*
|
|
* @param {!ChhotoJSON} result A successful Chhoto URL response.
|
|
* @returns {null}
|
|
*/
|
|
function notifySuccess(result) {
|
|
browser.notifications.create({
|
|
type: "basic",
|
|
title: "Shortened link copied!",
|
|
iconUrl: "icons/chhoto-url-64.png",
|
|
message: `${result.shorturl} was copied to your clipboard.`,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generates an error notification.
|
|
*
|
|
* @param {!Error} error An error with a message to notify users with.
|
|
* @returns {null}
|
|
*/
|
|
function notifyError(error) {
|
|
browser.notifications.create({
|
|
type: "basic",
|
|
title: "Failed to create a shortened link.",
|
|
iconUrl: "icons/chhoto-url-64.png",
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Main function for generating a shortened link.
|
|
*/
|
|
function generateChhoto() {
|
|
// Define the type of the request
|
|
const type = "background";
|
|
|
|
// Call functions
|
|
browser.tabs
|
|
.query({ active: true, currentWindow: true })
|
|
.then(tabData => validateURL(new URL(tabData[0].url), tabData[0].title), type)
|
|
.then(generateChhotoRequest)
|
|
.then(requestChhoto)
|
|
.then(validateChhotoResponse)
|
|
.then(copyLinkToClipboard)
|
|
.then(notifySuccess)
|
|
.catch(notifyError);
|
|
}
|
|
|
|
/**
|
|
* Function to generate a shortened link via the popup script (popup.js)
|
|
*/
|
|
function popupGenerateChhoto(url, title) {
|
|
validateURL(url, title, "popup")
|
|
.then(generateChhotoRequest)
|
|
.then(requestChhoto)
|
|
.then(validateChhotoResponse)
|
|
.then(copyLinkToClipboard)
|
|
.then(notifySuccess)
|
|
.catch(notifyError);
|
|
};
|
|
|
|
// When the extension icon is clicked, call the function above
|
|
browser.browserAction.onClicked.addListener(generateChhoto);
|
|
|
|
// Create a context menu
|
|
browser.contextMenus.create({
|
|
title: "Manually generate a Chhoto URL",
|
|
contexts: ["all"]
|
|
});
|
|
|
|
// Run code when the context menu is clicked
|
|
browser.contextMenus.onClicked.addListener( (info) => {
|
|
browser.windows.create({url: `/popup/popup.html?url=${info.pageUrl}`, type: "popup"});
|
|
});
|