2025-01-11 14:58:39 +10:00
/ *
* 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
2025-01-27 14:25:00 +10:00
* @ 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
2025-01-11 14:58:39 +10:00
* @ 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 .
2025-01-23 15:27:40 +10:00
* @ property { string } title The title of the website
2025-01-11 14:58:39 +10:00
* /
/ * *
* 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
2025-01-30 17:40:10 +10:00
* @ 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 .
2025-01-11 14:58:39 +10:00
* /
/ * *
* 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
2025-01-23 15:27:40 +10:00
* @ param { ! string } title
2025-01-27 14:25:00 +10:00
* @ param { ! string } type
* @ returns { ! Promise < [ URL , string , string ] , Error > }
2025-01-11 14:58:39 +10:00
* /
2025-01-27 14:25:00 +10:00
function validateURL ( url , title , type ) {
2025-01-11 14:58:39 +10:00
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:" ) ;
2025-01-13 16:00:18 +10:00
browser . storage . local . set ( { allowedProtocols : Array ( ... allowedProtocols ) } ) ;
} else {
allowedProtocols = new Set ( allowedProtocols ) ;
2025-01-11 14:58:39 +10:00
}
// 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. ` ) ) ;
}
2025-01-23 15:27:40 +10:00
// Return URL and title
2025-01-27 14:25:00 +10:00
return Promise . resolve ( [ url , title , type ] ) ;
2025-01-11 14:58:39 +10:00
} ) ;
}
/ * *
* Parses the URL outputted in the previous function , and gets the full link ( i . e . URI included ) .
*
2025-01-27 14:25:00 +10:00
* @ 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 .
2025-01-11 14:58:39 +10:00
* @ returns { ! Promise < ChhotoRequest , Error > }
* If all the data was obtained , return ChhotoRequest .
* Else , return an error
* /
2025-01-27 14:25:00 +10:00
function generateChhotoRequest ( [ url , title , type ] ) {
2025-01-11 14:58:39 +10:00
return browser . storage . local . get ( ) . then ( ( data ) => {
2025-01-27 14:25:00 +10:00
// 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."
) ) ;
}
2025-01-11 14:58:39 +10:00
// If the user didn't specify an API key
if ( ! data . chhotoKey ) {
return Promise . reject ( new Error (
2025-01-13 17:24:14 +10:00
"Missing API Key. Please configure the Chhoto URL extension. See https://git.solomon.tech/solomon/Chhoto-URL-Extension#installation for more information."
2025-01-11 14:58:39 +10:00
) ) ;
}
2025-01-23 15:27:40 +10:00
// Set URL and title
2025-01-11 14:58:39 +10:00
data . longUrl = url . href ;
2025-01-27 14:25:00 +10:00
// 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 = "" ;
}
2025-01-23 15:27:40 +10:00
2025-01-30 17:40:10 +10:00
// Set the type of the request
data . type = type ;
2025-01-23 15:27:40 +10:00
// If "generateWithTitle" is true
if ( data . generateWithTitle ) {
// Get the configured word limit
let wordLimit = data . titleWordLimit ;
// Format title name
2025-01-30 17:40:10 +10:00
// Trim all the whitespaces from the beginning and end of the string
2025-01-23 15:27:40 +10:00
// Replace all occurences of ' - ' to '-'
// Replace all occurences of ' ' to '-'
// Replace all characters except for 'a-z', '0-9', '-' and '_' with ''
2025-01-30 17:40:10 +10:00
let titleName = title . trim ( ) . toLowerCase ( ) . replace ( / - /g , '-' ) . replace ( /\s+/g , '-' ) . replace ( /[^a-z0-9-_]/g , '' ) ;
2025-01-23 15:27:40 +10:00
// If the wordLimit is not 0, and thus limited
2025-01-27 14:25:00 +10:00
// 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" ) {
2025-01-23 15:27:40 +10:00
// Limit the length of the short URL to the configured number
titleName = titleName . split ( '-' , wordLimit ) . join ( '-' ) ;
}
// Set the title
data . title = titleName ;
} ;
2025-01-11 14:58:39 +10:00
// 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 .
2025-01-23 15:27:40 +10:00
* @ returns { ! Promise < ChhotoResponse , Error > } The HTTP response from the Chhoto URL instance , or an error
2025-01-11 14:58:39 +10:00
* /
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!
2025-01-13 16:00:18 +10:00
headers . append ( "X-API-Key" , chhotoRequest . chhotoKey ) ;
2025-01-11 14:58:39 +10:00
// Return output of fetch
return fetch ( new Request (
` ${ chhotoRequest . chhotoHost } /api/new ` ,
{
method : 'POST' ,
headers ,
body : JSON . stringify ( {
2025-01-23 15:27:40 +10:00
shortlink : chhotoRequest . title ,
2025-01-11 14:58:39 +10:00
longlink : chhotoRequest . longUrl ,
} ) ,
} ,
2025-01-30 17:40:10 +10:00
// 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 } ) ) )
2025-01-27 14:25:00 +10:00
// If there was a HTTP error
// This does not activate if the Chhoto server returns a JSON response with an error
2025-01-11 14:58:39 +10:00
. 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 {
2025-01-30 17:40:10 +10:00
// If there was a URL conflict
2025-01-23 15:27:40 +10:00
if ( httpResp . status === 409 ) {
2025-01-30 17:40:10 +10:00
// 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
) ) ;
} ;
2025-01-23 15:27:40 +10:00
const json = {
success : true ,
error : false ,
2025-01-30 17:40:10 +10:00
shorturl : ` ${ httpResp . host } / ${ httpResp . requestedLink } ` ,
2025-01-23 15:27:40 +10:00
}
return json ;
}
2025-01-11 14:58:39 +10:00
return Promise . reject ( new Error (
` Error ( ${ httpResp . status } ): ${ httpResp . json . reason } . `
) ) ;
}
}
/ * *
* Copies the returned shortened link to the clipboard .
*
2025-01-30 17:40:10 +10:00
* @ param { ! ChhotoJSON } chhotoResp The JSON response from a Chhoto URL instance . * * May also include "type" * * if the request came from the popup . js script .
2025-01-11 14:58:39 +10:00
* @ returns { ! Promise < ChhotoJSON , Error > } ` ChhotoJSON ` , unmodified , on
* success , or an error indicating that we failed to copy to the clipboard .
* /
function copyLinkToClipboard ( chhotoResp ) {
2025-01-30 17:40:10 +10:00
// Send finished message
browser . runtime . sendMessage ( { message : "finished" } ) ;
2025-01-11 14:58:39 +10:00
// 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 ,
} ) ;
}
2025-01-27 14:25:00 +10:00
2025-01-11 14:58:39 +10:00
/ * *
* Main function for generating a shortened link .
* /
function generateChhoto ( ) {
2025-01-27 14:25:00 +10:00
// Define the type of the request
const type = "background" ;
// Call functions
2025-01-11 14:58:39 +10:00
browser . tabs
2025-01-27 14:25:00 +10:00
. 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" )
2025-01-11 14:58:39 +10:00
. then ( generateChhotoRequest )
. then ( requestChhoto )
. then ( validateChhotoResponse )
. then ( copyLinkToClipboard )
. then ( notifySuccess )
. catch ( notifyError ) ;
2025-01-27 14:25:00 +10:00
} ;
2025-01-11 14:58:39 +10:00
// When the extension icon is clicked, call the function above
browser . browserAction . onClicked . addListener ( generateChhoto ) ;
2025-01-27 14:25:00 +10:00
// Create a context menu
browser . contextMenus . create ( {
title : "Manually generate a Chhoto URL" ,
contexts : [ "all" ]
} ) ;
// Run code when the context menu is clicked
2025-01-27 17:26:25 +10:00
browser . contextMenus . onClicked . addListener ( ( info ) => {
browser . windows . create ( { url : ` /popup/popup.html?url= ${ info . pageUrl } ` , type : "popup" } ) ;
2025-01-27 14:25:00 +10:00
} ) ;